1.实验目的#
- 加深对进程同步与互斥概念的认识;
- 掌握信号量的使用,并应用它解决生产者——消费者问题;
- 掌握信号量的实现原理。
2.实验内容#
本次实验的基本内容是:
- 在 Ubuntu 下编写程序,用信号量解决生产者——消费者问题;
- 在 0.11 中实现信号量,用生产者—消费者程序检验之。
3.1 用信号量解决生产者—消费者问题#
在 Ubuntu 上编写应用程序“pc.c”,解决经典的生产者—消费者问题,完成下面的功能:
- 建立一个生产者进程,N 个消费者进程(N>1);
- 用文件建立一个共享缓冲区;
- 生产者进程依次向缓冲区写入整数 0,1,2,…,M,M>=500;
- 消费者进程从缓冲区读数,每次读一个,并将读出的数字从缓冲区删除,然后将本进程 ID 和 + 数字输出到标准输出;
- 缓冲区同时最多只能保存 10 个数。
一种可能的输出效果是:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
10: 0
10: 1
10: 2
10: 3
10: 4
11: 5
11: 6
12: 7
10: 8
12: 9
12: 10
12: 11
12: 12
……
11: 498
11: 499
|
其中 ID 的顺序会有较大变化,但冒号后的数字一定是从 0 开始递增加一的。
pc.c
中将会用到 sem_open()
、sem_close()
、sem_wait()
和 sem_post()
等信号量相关的系统调用,请查阅相关文档。
《UNIX 环境高级编程》是一本关于 Unix/Linux 系统级编程的相当经典的教程。如果你对 POSIX 编程感兴趣,建议买一本常备手边。
哈尔滨工业大学校园网用户可以在 ftp://run.hit.edu.cn/study/Computer_Science/Linux_Unix/ 下载,后续实验也用得到。
3.2 实现信号量#
Linux 在 0.11 版还没有实现信号量,Linus 把这件富有挑战的工作留给了你。如果能实现一套山寨版的完全符合 POSIX 规范的信号量,无疑是很有成就感的。但时间暂时不允许我们这么做,所以先弄一套缩水版的类 POSIX 信号量,它的函数原型和标准并不完全相同,而且只包含如下系统调用:
1
2
3
4
|
sem_t *sem_open(const char *name, unsigned int value);
int sem_wait(sem_t *sem);
int sem_post(sem_t *sem);
int sem_unlink(const char *name);
|
-
sem_open()
的功能是创建一个信号量,或打开一个已经存在的信号量。
sem_t
是信号量类型,根据实现的需要自定义。
name
是信号量的名字。不同的进程可以通过提供同样的 name 而共享同一个信号量。如果该信号量不存在,就创建新的名为 name 的信号量;如果存在,就打开已经存在的名为 name 的信号量。
value
是信号量的初值,仅当新建信号量时,此参数才有效,其余情况下它被忽略。当成功时,返回值是该信号量的唯一标识(比如,在内核的地址、ID 等),由另两个系统调用使用。如失败,返回值是 NULL。
-
sem_wait()
就是信号量的 P 原子操作。如果继续运行的条件不满足,则令调用进程等待在信号量 sem 上。返回 0 表示成功,返回 -1 表示失败。
-
sem_post()
就是信号量的 V 原子操作。如果有等待 sem 的进程,它会唤醒其中的一个。返回 0 表示成功,返回 -1 表示失败。
-
sem_unlink()
的功能是删除名为 name 的信号量。返回 0 表示成功,返回 -1 表示失败。
在 kernel
目录下新建 sem.c
文件实现如上功能。然后将 pc.c 从 Ubuntu 移植到 0.11 下,测试自己实现的信号量。
3.实验报告#
完成实验后,在实验报告中回答如下问题:
在 pc.c
中去掉所有与信号量有关的代码,再运行程序,执行效果有变化吗?为什么会这样? 实验的设计者在第一次编写生产者——消费者程序的时候,是这么做的:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
Producer()
{
// 生产一个产品 item;
// 空闲缓存资源
P(Empty);
// 互斥信号量
P(Mutex);
// 将item放到空闲缓存中;
V(Mutex);
// 产品资源
V(Full);
}
Consumer()
{
P(Full);
P(Mutex);
//从缓存区取出一个赋值给item;
V(Mutex);
// 消费产品item;
V(Empty);
}
|
这样可行吗?如果可行,那么它和标准解法在执行效果上会有什么不同?如果不可行,那么它有什么问题使它不可行?
4.实验提示#
本实验需要完成两个任务:(1)在 Ubuntu 下编写程序,用信号量解决生产者——消费者问题;(2)在 linux-0.11 中实现信号量,用生产者—消费者程序检验之。
4.1信号量#
信号量,英文为 semaphore,最早由荷兰科学家、图灵奖获得者 E. W. Dijkstra 设计,任何操作系统教科书的“进程同步”部分都会有详细叙述。
Linux 的信号量秉承 POSIX 规范,用man sem_overview
可以查看相关信息。
本次实验涉及到的信号量系统调用包括:sem_open()
、sem_wait()
、sem_post()
和 sem_unlink()
。
生产者—消费者问题
生产者—消费者问题的解法几乎在所有操作系统教科书上都有,其基本结构为:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
Producer()
{
// 生产一个产品 item;
// 空闲缓存资源
P(Empty);
// 互斥信号量
P(Mutex);
// 将item放到空闲缓存中;
V(Mutex);
// 产品资源
V(Full);
}
Consumer()
{
P(Full);
P(Mutex);
//从缓存区取出一个赋值给item;
V(Mutex);
// 消费产品item;
V(Empty);
}
|
显然在演示这一过程时需要创建两类进程,一类执行函数 Producer()
,另一类执行函数 Consumer()
。
4.2多进程共享文件#
在 Linux 下使用 C 语言,可以通过三种方法进行文件的读写:
- 使用标准 C 的
fopen()
、fread()
、fwrite()
、fseek()
和 fclose()
等;
- 使用系统调用
open()
、read()
、write()
、lseek()
和 close()
等;
- 通过内存镜像文件,使用
mmap()
系统调用。
- 在 Linux 0.11 上只能使用前两种方法。
fork()
调用成功后,子进程会继承父进程拥有的大多数资源,包括父进程打开的文件。所以子进程可以直接使用父进程创建的文件指针/描述符/句柄,访问的是与父进程相同的文件。
使用标准 C 的文件操作函数要注意,它们使用的是进程空间内的文件缓冲区,父进程和子进程之间不共享这个缓冲区。因此,任何一个进程做完写操作后,必须 fflush()
一下,将数据强制更新到磁盘,其它进程才能读到所需数据。
建议直接使用系统调用进行文件操作。
4.3终端也是临界资源#
用 printf() 向终端输出信息是很自然的事情,但当多个进程同时输出时,终端也成为了一个临界资源,需要做好互斥保护,否则输出的信息可能错乱。
另外,printf() 之后,信息只是保存在输出缓冲区内,还没有真正送到终端上,这也可能造成输出信息时序不一致。用 fflush(stdout)
可以确保数据送到终端。
4.4原子操作、睡眠和唤醒#
Linux 0.11 是一个支持并发的现代操作系统,虽然它还没有面向应用实现任何锁或者信号量,但它内部一定使用了锁机制,即在多个进程访问共享的内核数据时一定需要通过锁来实现互斥和同步。
锁必然是一种原子操作。通过模仿 0.11 的锁,就可以实现信号量。
多个进程对磁盘的并发访问是一个需要锁的地方。Linux 0.11 访问磁盘的基本处理办法是在内存中划出一段磁盘缓存,用来加快对磁盘的访问。进程提出的磁盘访问请求首先要到磁盘缓存中去找,如果找到直接返回;如果没有找到则申请一段空闲的磁盘缓存,以这段磁盘缓存为参数发起磁盘读写请求。请求发出后,进程要睡眠等待(因为磁盘读写很慢,应该让出 CPU 让其他进程执行)。这种方法是许多操作系统(包括现代 Linux、UNIX 等)采用的较通用的方法。这里涉及到多个进程共同操作磁盘缓存,而进程在操作过程可能会被调度而失去 CPU。因此操作磁盘缓存时需要考虑互斥问题,所以其中必定用到了锁。而且也一定用到了让进程睡眠和唤醒。
下面是从 kernel/blk_drv/ll_rw_blk.c
文件中取出的两个函数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
static inline void lock_buffer(struct buffer_head * bh)
{
// 关中断
cli();
// 将当前进程睡眠在 bh->b_wait
while (bh->b_lock)
sleep_on(&bh->b_wait);
bh->b_lock=1;
// 开中断
sti();
}
static inline void unlock_buffer(struct buffer_head * bh)
{
if (!bh->b_lock)
printk("ll_rw_block.c: buffer not locked\n\r");
bh->b_lock = 0;
// 唤醒睡眠在 bh->b_wait 上的进程
wake_up(&bh->b_wait);
}
|
分析 lock_buffer()
可以看出,访问锁变量时用开、关中断来实现原子操作,阻止进程切换的发生。当然这种方法有缺点,且不适合用于多处理器环境中,但对于 Linux 0.11,它是一种简单、直接而有效的机制。
另外,上面的函数表明 Linux 0.11 提供了这样的接口:用 sleep_on()
实现进程的睡眠,用 wake_up()
实现进程的唤醒。它们的参数都是一个结构体指针—— struct task_struct *
,即进程都睡眠或唤醒在该参数指向的一个进程 PCB 结构链表上。
因此,我们可以用开关中断的方式实现原子操作,而调用 sleep_on()
和 wake_up()
进行进程的睡眠和唤醒。
sleep_on()
的功能是将当前进程睡眠在参数指定的链表上(注意,这个链表是一个隐式链表,详见《注释》一书)。wake_up()
的功能是唤醒链表上睡眠的所有进程。这些进程都会被调度运行,所以它们被唤醒后,还要重新判断一下是否可以继续运行。可参考 lock_buffer()
中的那个 while 循环。
4.5应对混乱的 bochs 虚拟屏幕#
不知是 Linux 0.11 还是 bochs 的 bug,如果向终端输出的信息较多,bochs 的虚拟屏幕会产生混乱。此时按 ctrl+L 可以重新初始化一下屏幕,但输出信息一多,还是会混乱。建议把输出信息重定向到一个文件,然后用 vi、more 等工具按屏查看这个文件,可以基本解决此问题。
4.6关于 string.h 的提示#
下面描述的问题未必具有普遍意义,仅做为提醒,请实验者注意。
include/string.h 实现了全套的 C 语言字符串操作,而且都是采用汇编 + inline 方式优化。
但在使用中,某些情况下可能会遇到一些奇怪的问题。比如某人就遇到 strcmp()
会破坏参数内容的问题。如果调试中遇到有些 “诡异” 的情况,可以试试不包含头文件,一般都能解决。不包含 string.h
,就不会用 inline 方式调用这些函数,它们工作起来就趋于正常了。
5.实验步骤#
实验内容#
本次实验的基本内容是在Linux 0.11的内核中实现信号量,并向用户提供使用信号量的接口,用户使用该接口解决一个实际的进程同步问题。实验的主要内容包括如下两个部分:
1.实现信号量#
在Linux 0.11内核上(Linux 0.11内核中没有定义信号量)实现信号量,并创建相应的系统调用以供用户使用。应提供的系统接口主要包括:
1
|
int CreateSemaphore(char * semname);
|
该操作用来在内核中创建一个信号量,输入的参数是信号量的名字,返回的是信号量的一个整数标识semid,信号量是一种内核资源,不应该无限制的创建,所以在内核中信号量可以被组织成一个数组,此时semid就是创建的信号量在内核信号量数组中的下标。如果这个名为semname的信号量已经创建,则返回这个已创建的信号量的标识semid,也即多个具有相同名字的信号量创建接口返回相同的返回值,即相同的semid
1
|
int SetSemaphore(int semid, int value);
|
用来设置信号量的值,其中semid是信号量标识,value是要设置的信号量值,该函数的返回值是信号量的当前值。该接口通常用来设置信号量的初值。
1
|
int WaitSemaphore(int semid);
|
该函数就是信号量的P操作,其功能就是对信号量的值减1,如果其值小于0则令调用进程等待在信号量semid上。
1
|
int SignalSemaphore(int semid);
|
该函数就是信号量的V操作,其功能就是对信号量的值加1,如果其值小于等于0则令唤醒等待在信号量semid上的进程。
2.使用信号量#
在定义了信号量的Linux 0.11操作系统上编写用户程序来演示信号量的作用。该用户程序解决就是传统的生产者—消费者问题,要求编写的用户程序完成下面的任务:
1.编写的主程序演示生产者—消费者两个进程的同步过程;
2.编写的主程序创建两个进程:生产者进程和消费者进程;
3.编写生产者进程和消费者进程的代码。
4.要求对比三种设置下的运行结果:
- 没有信号量下的生产者—消费者。
- 有信号量,1个生产者进程,1个消费者进程,用for循环控制生产者(消费者)各执行N次。
- 有信号量,N个生产者进程,N个消费者进程。
一、实现信号量#
1.新建sem.h#
在linux-0.11/include/linux目录下新建sem.h,定义信号量的数据结构。sem.h的代码如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
#ifndef _SEM_H
#define _SEM_H
#include <linux/sched.h>
#define SEMTABLE_LEN 20
#define SEM_NAME_LEN 20
typedef struct semaphore{
char name[SEM_NAME_LEN];
int value;
struct task_struct *queue;
} sem_t;
extern sem_t semtable[SEMTABLE_LEN];
#endif
|
代码截图如下:
2.新建sem.c#
在linux-0.11/kernel目录下,新建实现信号量函数的源代码文件sem.c。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
|
#include <linux/sem.h>
#include <linux/sched.h>
#include <unistd.h>
#include <asm/segment.h>
#include <linux/tty.h>
#include <linux/kernel.h>
#include <linux/fdreg.h>
#include <asm/system.h>
#include <asm/io.h>
//#include <string.h>
sem_t semtable[SEMTABLE_LEN];
int cnt = 0;
sem_t *sys_sem_open(const char *name,unsigned int value)
{
char kernelname[100];
int isExist = 0;
int i=0;
int name_cnt=0;
while( get_fs_byte(name+name_cnt) != '\0')
name_cnt++;
if(name_cnt>SEM_NAME_LEN)
return NULL;
for(i=0;i<name_cnt;i++)
kernelname[i]=get_fs_byte(name+i);
int name_len = strlen(kernelname);
int sem_name_len =0;
sem_t *p=NULL;
for(i=0;i<cnt;i++)
{
sem_name_len = strlen(semtable[i].name);
if(sem_name_len == name_len)
{
if( !strcmp(kernelname,semtable[i].name) )
{
isExist = 1;
break;
}
}
}
if(isExist == 1)
{
p=(sem_t*)(&semtable[i]);
//printk("find previous name!\n");
}
else
{
i=0;
for(i=0;i<name_len;i++)
{
semtable[cnt].name[i]=kernelname[i];
}
semtable[cnt].value = value;
p=(sem_t*)(&semtable[cnt]);
//printk("creat name!\n");
cnt++;
}
return p;
}
int sys_sem_wait(sem_t *sem)
{
cli();
while( sem->value <= 0 ) //
sleep_on(&(sem->queue)); //这两条语句顺序不能颠倒,很重要,是关于互斥信号量能不能正确工作的!!!
sem->value--;
sti();
return 0;
}
int sys_sem_post(sem_t *sem)
{
cli();
sem->value++;
if( (sem->value) <= 1)
wake_up(&(sem->queue));
sti();
return 0;
}
int sys_sem_unlink(const char *name)
{
char kernelname[100]; /* 应该足够大了 */
int isExist = 0;
int i=0;
int name_cnt=0;
while( get_fs_byte(name+name_cnt) != '\0')
name_cnt++;
if(name_cnt>SEM_NAME_LEN)
return NULL;
for(i=0;i<name_cnt;i++)
kernelname[i]=get_fs_byte(name+i);
int name_len = strlen(name);
int sem_name_len =0;
for(i=0;i<cnt;i++)
{
sem_name_len = strlen(semtable[i].name);
if(sem_name_len == name_len)
{
if( !strcmp(kernelname,semtable[i].name))
{
isExist = 1;
break;
}
}
}
if(isExist == 1)
{
int tmp=0;
for(tmp=i;tmp<=cnt;tmp++)
{
semtable[tmp]=semtable[tmp+1];
}
cnt = cnt-1;
return 0;
}
else
return -1;
}
|
3.修改unistd.h#
在unistd.h内增加新的系统调用编号(之前的实验中也有做系统调用,所以这里直接记录一下修改的结果,过程就大概省略了一些)
1
2
3
4
|
#define __NR_sem_open 72
#define __NR_sem_wait 73
#define __NR_sem_post 74
#define __NR_sem_unlink 75
|
4.修改system_call.s#
在system_call.s文件中找到nr_system_calls并将其值更改为76(因为增加了四个sem系统调用函数)
5.修改sys.h#
1
2
3
4
|
extern int sys_sem_open();
extern int sys_sem_wait();
extern int sys_sem_post();
extern int sys_sem_unlink();
|
1
|
sys_sem_open,sys_sem_wait,sys_sem_post,sys_sem_unlink
|
代码截图如下:
6.修改Makefile#
将kernel下的Makefile修改为如下代码块(部分):
1
2
3
4
5
6
7
8
9
10
11
|
......
OBJS = sched.o system_call.o traps.o asm.o fork.o \
panic.o printk.o vsprintf.o sys.o exit.o \
signal.o mktime.o sem.o
......
### ###Dependencies:
sem.s sem.o: sem.c ../include/linux/sem.h ../include/linux/kernel.h \
../include/unistd.h
......
|
Makefile截图如下:
7.挂载hdc并准备相关文件#
进入oslab根目录执行sudo ./mount-hdc,随后将unistd.h复制到usr/include下,将sem.h复制到usr/include/linux下,最后使用sudo umount hdc卸载hdc
二、编写生产者-消费者检验程序#
1.新建pc.c文件#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
|
#define __LIBRARY__
#include <unistd.h>
#include <linux/sem.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <linux/sched.h>
_syscall2(sem_t *,sem_open,const char *,name,unsigned int,value)
_syscall1(int,sem_wait,sem_t *,sem)
_syscall1(int,sem_post,sem_t *,sem)
_syscall1(int,sem_unlink,const char *,name)
const char *FILENAME = "/usr/root/buffer_file"; /* 消费生产的产品存放的缓冲文件的路径 */
const int NR_CONSUMERS = 5; /* 消费者的数量 */
const int NR_ITEMS = 50; /* 产品的最大量 */
const int BUFFER_SIZE = 10; /* 缓冲区大小,表示可同时存在的产品数量 */
sem_t *metux, *full, *empty; /* 3个信号量 */
unsigned int item_pro, item_used; /* 刚生产的产品号;刚消费的产品号 */
int fi, fo; /* 供生产者写入或消费者读取的缓冲文件的句柄 */
int main(int argc, char *argv[])
{
char *filename;
int pid;
int i;
filename = argc > 1 ? argv[1] : FILENAME;
/* O_TRUNC 表示:当文件以只读或只写打开时,若文件存在,则将其长度截为0(即清空文件)
* 0222 和 0444 分别表示文件只写和只读(前面的0是八进制标识)
*/
fi = open(filename, O_CREAT| O_TRUNC| O_WRONLY, 0222); /* 以只写方式打开文件给生产者写入产品编号 */
fo = open(filename, O_TRUNC| O_RDONLY, 0444); /* 以只读方式打开文件给消费者读出产品编号 */
metux = sem_open("METUX", 1); /* 互斥信号量,防止生产消费同时进行 */
full = sem_open("FULL", 0); /* 产品剩余信号量,大于0则可消费 */
empty = sem_open("EMPTY", BUFFER_SIZE); /* 空信号量,它与产品剩余信号量此消彼长,大于0时生产者才能继续生产 */
item_pro = 0;
if ((pid = fork())) /* 父进程用来执行消费者动作 */
{
printf("pid %d:\tproducer created....\n", pid);
/* printf()输出的信息会先保存到输出缓冲区,并没有马上输出到标准输出(通常为终端控制台)。
* 为避免偶然因素的影响,我们每次printf()都调用一下stdio.h中的fflush(stdout)
* 来确保将输出立刻输出到标准输出。
*/
fflush(stdout);
while (item_pro <= NR_ITEMS) /* 生产完所需产品 */
{
sem_wait(empty);
sem_wait(metux);
/* 生产完一轮产品(文件缓冲区只能容纳BUFFER_SIZE个产品编号)后
* 将缓冲文件的位置指针重新定位到文件首部。
*/
if(!(item_pro % BUFFER_SIZE))
lseek(fi, 0, 0);
write(fi, (char *) &item_pro, sizeof(item_pro)); /* 写入产品编号 */
printf("pid %d:\tproduces item %d\n", pid, item_pro);
fflush(stdout);
item_pro++;
sem_post(metux);
sem_post(full); /* 唤醒消费者进程 */
}
}
else /* 子进程来创建消费者 */
{
i = NR_CONSUMERS;
while(i--)
{
if(!(pid=fork())) /* 创建i个消费者进程 */
{
pid = getpid();
printf("pid %d:\tconsumer %d created....\n", pid, NR_CONSUMERS-i);
fflush(stdout);
while(1)
{
sem_wait(full);
sem_wait(metux);
/* read()读到文件末尾时返回0,将文件的位置指针重新定位到文件首部 */
if(!read(fo, (char *)&item_used, sizeof(item_used)))
{
lseek(fo, 0, 0);
read(fo, (char *)&item_used, sizeof(item_used));
}
printf("pid %d:\tconsumer %d consumes item %d\n", pid, NR_CONSUMERS-i+1, item_used);
fflush(stdout);
sem_post(metux);
sem_post(empty); /* 唤醒生产者进程 */
if(item_used == NR_ITEMS) /* 如果已经消费完最后一个商品,则结束 */
goto OK;
}
}
}
}
OK:
close(fi);
close(fo);
return 0;
}
|
将此文件移动到usr/root目录下,此目录需要挂载hdc才可以访问。移动成功之后直接重新编译linux并在虚拟环境内运行。
2.编译运行pc.c#
运行linux-0.11之后,首先编译pc.c,使用命令gcc -o pc pc.c,随后运行pc,使用命令**./pc > sem_output**即可,最终在虚拟环境内输入sync把修改的数据写入磁盘。
遇到的问题(还没解决)
#
上面报了一个类型转换的异常,定义和调用sem_open的地方如下,但是没有发现哪个地方写错了
3.查看sem_output#
首先挂载hdc,然后进入usr/root目录并在终端内执行sudo less sem_output命令,可看到下图结果:
4.对比有无信号量#
删除pc.c文件中关于信号量的代码,重新编译运行后得出如下结果
回答问题#
在有无信号量的不同条件下对比运行结果后可以发现,如果去掉所有与信号量有关的代码,编译运行程序之后可以发现输出的数字顺序完全混乱。
信号量不存在的情况下,进程之间无法同步或者协作,造成此种情况的有如下原因:
- 一种情况是缓冲区满了,生产者还在写入数据,会造覆盖掉部分数据。
- 一种是缓冲区为空,消费者尝试读取数据,读到的数据是已输出的数据。
- 多个进程对文件缓冲区同时访问,造成了程序崩溃。