标签:
从一个实际的问题:生产者与消费者出发,谈一谈为什么要有信号量?信号量用来做什么?
问题描述:现在存在一个文件”.\buffer.txt”作为一个共享缓冲区,缓冲区同时最多只能保存10个数。现有一个生产者进程,依次向缓冲区写入整数0,1,2,……,M,M>=500;有N个消费者进程,消费者进程从缓冲区读数,每次读一个,并将读出的数从缓冲区删除。
信号量有什么组成?
同时,由于该value的值是所有进程都可以看到和访问的共享变量,所以必须在内核中定义;同样,这个名字的信号量也是可供所有进程访问的,必须在内核中定义;同时,又要操作内核中的数据结构:进程控制块PCB,所以信号量一定要在内核中定义,而且必须是全局变量。由于信号量要定义在内核中,所以和信号量相关的操作函数也必须做成系统调用,还是那句话:系统调用是应用程序访问内核的唯一方法。
和信号量相关的函数
Linux在0.11版还没有实现信号量,我们可以先弄一套缩水版的类POSIX信号量,它的函数原型和标准并不完全相同,而且只包含如下系统调用:
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_t是信号量类型,根据实现的需要自己定义。
信号量的保护
使用信号量还需要注意一个问题,这个问题是由多进程的调度引起的。当一个进程正在修改信号量的值时,由于时间片耗完,引发调度,该修改信号量的进程被切换出去,而得到CPU使用权的新进程也开始修改此信号量,那么该信号量的值就很有可能发生错误,如果信号量的值出错了,那么进程的同步也会出错。所以在执行修改信号量的代码时,必须加以保护,保证在修改过程中其他进程不能修改同一个信号量的值。也就是说,当一个进程在修改信号量时,由于某种原因引发调度,该进程被切换出去,新的进程如果也想修改该信号量,是不能操作的,必须等待,直到原来修改该信号量的进程完成修改,其他进程才能修改此信号量。修改信号量的代码一次只允许一个进程执行,这样的代码称为临界区,所以信号量的保护,又称临界区保护。
实现临界区的保护有几种不同的方法,在Linux 0.11上比较简单的方法是通过开、关中断来阻止时钟中断,从而避免因时间片耗完引发的调度,来实现信号量的保护。但是开关中断的方法,只适合单CPU的情况,对于多CPU的情况,不适用。Linux 0.11就是单CPU,可以使用这种方法。
sem_open()
原型:sem_t *sem_open(const char *name, unsigned int value)
功能:创建一个信号量,或打开一个已经存在的信号量
参数:
由于要做成系统调用,所以会穿插讲解系统调用的相关知识。
首先,在linux-0.11/kernel目录下,新建实现信号量函数的源代码文件sem.c。同时,在linux-0.11/include/linux目录下新建sem.h,定义信号量的数据结构。
linux-0.11/include/linux/sem.h
1 #ifndef _SEM_H 2 #define _SEM_H 3 4 #include <linux/sched.h> 5 6 #define SEMTABLE_LEN 20 7 #define SEM_NAME_LEN 20 8 9 10 typedef struct semaphore{ 11 char name[SEM_NAME_LEN]; 12 int value; 13 struct task_struct *queue; 14 } sem_t; 15 extern sem_t semtable[SEMTABLE_LEN]; 16 17 #endif
由于sem_open()的第一个参数name,传入的是应用程序所在地址空间的逻辑地址,在内核中如果直接访问这个地址,访问到的是内核空间中的数据,不会是用户空间的。所以要用get_fs_byte()函数获取用户空间的数据。get_fs_byte()函数的功能是获得一个字节的用户空间中的数据。同样,sem_unlink()函数的参数name也要进行相同的处理。
int sem_unlink(const char *name)
sem_wait()
原型:int sem_wait(sem_t *sem)
功能:信号量的P原子操作(检查信号量是不是为负值,如果是,则停下来睡眠等待,如果不是,则向下执行)。
返回值:返回0表示成功,返回-1表示失败。
sem_post()
原型:int sem_post(sem_t *sem)
功能:信号量的V原子操作(检查信号量的值是不是为0,如果是,表示有进程在睡眠等待,则唤醒队首进程,如果不是,向下执行)。
返回值:返回0表示成功,返回-1表示失败。
我们可以利用linux 0.11提供的函数sleep_on()实现进程的睡眠,用wake_up()实现进程的唤醒。
但是,sleep_on()比较难以理解。我们先看下sleep_on()的源码。
1 void sleep_on(struct task_struct **p) 2 { 3 struct task_struct *tmp; 4 5 if (!p) 6 return; 7 if (current == &(init_task.task)) 8 panic("task[0] trying to sleep"); 9 tmp = *p; 10 *p = current; 11 current->state = TASK_UNINTERRUPTIBLE; 12 schedule(); 13 if (tmp) 14 tmp->state=0; 15 }
还拿生产者和消费者的例子来说,依然是有一个生产者和N个消费者,目前缓冲区为空,没有数可取。
消费者C1请求取数,调用sleep_on(&sem->queue)。此时,tmp指向NULL,p指向C1,调用schedule(),让出CPU的使用权。此时,信号量sem处等待队列的情况如下:
由于tmp是进程C1调用sleep_on()
函数时申请的局部变量,所以会保存在C1运行到sleep_on()
函数中时C1的内核栈中,只要进程C1还没有从sleep_on()
函数中退出,tmp就会一直保存在C1的内核栈中。而进程C1是在sleep_on()
中调用schedule()
切出去的,所以在C1睡眠期间,tmp自然会保存在C1的内核栈中。这一点对于理解sleep_on()
上如何形成隐式的等待队列很重要。
消费者C2请求取数,调用sleep_on(&sem->queue)。此时,信号量sem处的等待队列如下:
从这里就可以看到隐式的等待队列已经形成了。由于进程C2也会由于调用schedule()
函数在sleep_on()
函数中睡眠,所以进程C2内核栈上的tmp便指向之前的等待队列的队首,也就是C1,通过C2的内核栈便可以找到睡眠的进程C1。这样就可以找到在信号量sem处睡眠的所有进程。
我们在看下唤醒函数wake_up()
:
1 void wake_up(struct task_struct **p) 2 { 3 if (p && *p) { 4 (**p).state=0; 5 *p=NULL; 6 } 7 }
从中我们可以看到唤醒函数wake_up()
负责唤醒的是等待队列队首的进程。
当队首进程C2被唤醒时,从schedule()
函数退出,执行语句:
1 if (tmp) 2 tmp->state=0;
会将内核栈上由tmp指向的进程C1唤醒,如果进程C1的tmp还指向其他睡眠的进程,当C1被调度执行时,会将其tmp指向的进程唤醒,这样只要执行一次wake_up()
操作,就可以依次将所有等待在信号量sem处的睡眠进程唤醒。
由于我们要调用sleep_on()
实现进程的睡眠,调用wake_up()
实现进程的唤醒,我们在上面已经讲清楚了sleep_on()
和wake_up()
的工作机制,接下来,便可以具体实现sem_wait()
和sem_post()
函数了。
sem_wait()的实现
考虑到sleep_on()
会形成一个隐式的等待队列,而wake_up()
只要唤醒了等待队列的头结点,就可以依靠sleep_on()
内部的判断语句,实现依次唤醒全部的等待进程。所以,sem_wait()
的代码实现,必须考虑到这个情况。
参考linux 0.11内部的代码,对于进程是否需要等待的判断,不能用简单的if语句,而应该用while()语句,假设现在sem=-1,生产者往缓冲区写入了一个数,sem=0<=0,此时应该将等待队列队首的进程唤醒。当被唤醒的队首进程再次调度执行,从sleep_on()
函数退出,不会再执行if判断,而直接从if语句退出,继续向下执行。而等待队列后面被唤醒的进程随后也会被调度执行,同样也不会执行if判断,退出if语句,继续向下执行,这显然是不应该的。因为生产者只往缓冲区写入了一个数,被等待队列的队首进程取走了,由于等待队列的队首进程已经取走了那个数,它应该已经将sem修改为sem=-1,其他等待的进程应该再次执行if判断,由于sem=-1<0,会继续睡眠。要让其他等待进程再次执行时,要重新进行判断,所以不能是if语句了,必须是while()语句才可以。
下面是我第一次实现sem_wait()
的代码:
1 int sys_sem_wait(sem_t *sem) 2 { 3 cli(); 4 sem->value--; 5 while( sem->value < 0 ) 6 sleep_on(&(sem->queue)) 7 sti(); 8 return 0; 9 }
但是没有考虑到有一种特殊的信号量:互斥信号量。比如要读写一个文件,一次只能允许一个进程读写,当一个进程要读写该文件时,需要先执行sem_wait(file)
,此后在该进程读写文件期间,若有其他进程也要读写该文件,则执行流程分析如下:
sleep_on(&file->queue)
。sleep_on(&file->queue)
。sleep_on()
函数形成的隐式的等待队列已经记录下了进程的等待情况。正确的sem_wait()
代码如下:
1 int sys_sem_wait(sem_t *sem) 2 { 3 cli(); 4 while( sem->value <= 0 ) // 5 sleep_on(&(sem->queue)); //这两条语句顺序不能颠倒,很重要,是关于互斥信号量能不 6 sem->value--; //能正确工作的!!! 7 sti(); 8 return 0; 9 }
sem_post()的实现
sem_post
的实现必须结合sem_wait()
的实现情况。
还拿生产者和消费者的例子来分析。当前缓冲区为空,没有数可取,value=0。
sem_wait()
,value=0,sleep_on(&queue)
。sem_wait()
,value=0,sleep_on(&queue)
。等待队列的情况如下: sem_post()
,value=1,wake_up(&queue)
,唤醒消费者C2。队列的情况如下: sem_post()
,value=2,wake_up(&queue)
相当于wake_up(NULL)
。队列情况如上。由此可以看出,sem_post()
里面唤醒进程的判断条件是:value<=1。
sem_post
的实现代码如下:
1 int sys_sem_post(sem_t *sem) 2 { 3 cli(); 4 sem->value++; 5 if( (sem->value) <= 1) 6 wake_up(&(sem->queue)); 7 sti(); 8 return 0; 9 }
linux-0.11/kernel/sem.c
1 #include <linux/sem.h> 2 #include <linux/sched.h> 3 #include <unistd.h> 4 #include <asm/segment.h> 5 #include <linux/tty.h> 6 #include <linux/kernel.h> 7 #include <linux/fdreg.h> 8 #include <asm/system.h> 9 #include <asm/io.h> 10 //#include <string.h> 11 12 sem_t semtable[SEMTABLE_LEN]; 13 int cnt = 0; 14 15 sem_t *sys_sem_open(const char *name,unsigned int value) 16 { 17 char kernelname[100]; /* 应该足够大了 */ 18 int isExist = 0; 19 int i=0; 20 int name_cnt=0; 21 while( get_fs_byte(name+name_cnt) != ‘\0‘) 22 name_cnt++; 23 if(name_cnt>SEM_NAME_LEN) 24 return NULL; 25 for(i=0;i<name_cnt;i++) 26 kernelname[i]=get_fs_byte(name+i); 27 int name_len = strlen(kernelname); 28 int sem_name_len =0; 29 sem_t *p=NULL; 30 for(i=0;i<cnt;i++) 31 { 32 sem_name_len = strlen(semtable[i].name); 33 if(sem_name_len == name_len) 34 { 35 if( !strcmp(kernelname,semtable[i].name) ) 36 { 37 isExist = 1; 38 break; 39 } 40 } 41 } 42 if(isExist == 1) 43 { 44 p=(sem_t*)(&semtable[i]); 45 //printk("find previous name!\n"); 46 } 47 else 48 { 49 i=0; 50 for(i=0;i<name_len;i++) 51 { 52 semtable[cnt].name[i]=kernelname[i]; 53 } 54 semtable[cnt].value = value; 55 p=(sem_t*)(&semtable[cnt]); 56 //printk("creat name!\n"); 57 cnt++; 58 } 59 return p; 60 } 61 62 63 int sys_sem_wait(sem_t *sem) 64 { 65 cli(); 66 while( sem->value <= 0 ) // 67 sleep_on(&(sem->queue)); //这两条语句顺序不能颠倒,很重要,是关于互斥信号量能不 68 sem->value--; //能正确工作的!!! 69 sti(); 70 return 0; 71 } 72 int sys_sem_post(sem_t *sem) 73 { 74 cli(); 75 sem->value++; 76 if( (sem->value) <= 1) 77 wake_up(&(sem->queue)); 78 sti(); 79 return 0; 80 } 81 82 int sys_sem_unlink(const char *name) 83 { 84 char kernelname[100]; /* 应该足够大了 */ 85 int isExist = 0; 86 int i=0; 87 int name_cnt=0; 88 while( get_fs_byte(name+name_cnt) != ‘\0‘) 89 name_cnt++; 90 if(name_cnt>SEM_NAME_LEN) 91 return NULL; 92 for(i=0;i<name_cnt;i++) 93 kernelname[i]=get_fs_byte(name+i); 94 int name_len = strlen(name); 95 int sem_name_len =0; 96 for(i=0;i<cnt;i++) 97 { 98 sem_name_len = strlen(semtable[i].name); 99 if(sem_name_len == name_len) 100 { 101 if( !strcmp(kernelname,semtable[i].name)) 102 { 103 isExist = 1; 104 break; 105 } 106 } 107 } 108 if(isExist == 1) 109 { 110 int tmp=0; 111 for(tmp=i;tmp<=cnt;tmp++) 112 { 113 semtable[tmp]=semtable[tmp+1]; 114 } 115 cnt = cnt-1; 116 return 0; 117 } 118 else 119 return -1; 120 }
应用程序包含的宏定义和头文件
由于系统调用是借助内嵌汇编_syscall实现的,而_syscall的内嵌汇编实现是在linux-0.11/include/unistd.h中,所以必须包含#include <unistd.h>
这个头文件,另外由于_syscall的内嵌汇编实现是包含在一个条件编译里面,所以必须包含这样一个宏定义#define __LIBRARY__
。
修改unistd.h
添加我们新增的系统调用的编号。
添加的代码如下:
1 #define __NR_sem_open 72 /* !!! */ 2 #define __NR_sem_wait 73 3 #define __NR_sem_post 74 4 #define __NR_sem_unlink 75
修改system_call.s
由于新增了4个系统调用,所以需要修改总的系统调用的和值。
修改代码如下:
1 nr_system_calls = 76 /* !!! */
修改sys.h
要在linux-0.11/include/linux/sys.h中,声明这4个新增的函数。
修改代码如下:
1 extern int sys_sem_open(); 2 extern int sys_sem_wait(); 3 extern int sys_sem_post(); 4 extern int sys_sem_unlink(); 5 6 fn_ptr sys_call_table[] = { sys_setup, sys_exit, sys_fork, sys_read, 7 sys_write, sys_open, sys_close, sys_waitpid, sys_creat, sys_link, 8 sys_unlink, sys_execve, sys_chdir, sys_time, sys_mknod, sys_chmod, 9 sys_chown, sys_break, sys_stat, sys_lseek, sys_getpid, sys_mount, 10 sys_umount, sys_setuid, sys_getuid, sys_stime, sys_ptrace, sys_alarm, 11 sys_fstat, sys_pause, sys_utime, sys_stty, sys_gtty, sys_access, 12 sys_nice, sys_ftime, sys_sync, sys_kill, sys_rename, sys_mkdir, 13 sys_rmdir, sys_dup, sys_pipe, sys_times, sys_prof, sys_brk, sys_setgid, 14 sys_getgid, sys_signal, sys_geteuid, sys_getegid, sys_acct, sys_phys, 15 sys_lock, sys_ioctl, sys_fcntl, sys_mpx, sys_setpgid, sys_ulimit, 16 sys_uname, sys_umask, sys_chroot, sys_ustat, sys_dup2, sys_getppid, 17 sys_getpgrp, sys_setsid, sys_sigaction, sys_sgetmask, sys_ssetmask, 18 sys_setreuid,sys_setregid,sys_sem_open,sys_sem_wait,sys_sem_post,sys_sem_unlink };
修改linux-0.11/kernel目录下的Makefile
修改代码如下:
...... 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 ......
在0.11环境下的/usr/include目录下,将修改过的unistd.h文件拷贝覆盖那里原有的unistd.h文件。
基本要求
- 建立一个生产者进程,N个消费者进程( N>1 )
- 用文件建立一个共享缓冲区
- 生产者进程依次向缓冲区写入整数0,1,2,…,M,M>=500
- 消费者进程从缓冲区读数,每次读一个,并将读出的数字从缓冲区删除,然后将本进程ID和+ 数字输出到标准输出
- 缓冲区同时最多只能保存10个数
一种可能的输出效果是:
10: 0
文件IO函数
由于要用文件建立一个共享缓冲区,同时生产者要往文件中写数,消费者要从文件中读数,所以要用到open()、read()、write()、lseek()、close()这些文件IO系统调用。
应用程序实现的难点在于,消费者进程每次读一个数,要将读出的数字从缓冲区删除,这几个文件IO系统调用函数中,并没有可以删除一个数字的函数。解决办法是,当消费者进程要从缓冲区读数时,首先调用lseek()系统调用获取到目前文件指针的位置,保存生产者目前写文件的位置。由于被消费者进程读过的数都被删除了,所以同时最多只能保存10个数的缓冲区已有的数,一定是消费者进程未读的,也就是说每次消费者要从缓冲区读数时,要读的数一定是缓冲区的第一个数。这样,让消费者进程每次都从缓冲区读10个数出来,取读出的10个数中的第一个数送标准输出显示,再将后面的9个数再次写入到缓冲区中,这样,就可以做到删除读出的那个数。最后,再调用lseek()系统调用将文件指针定位到之前保存的文件指针减1的位置,这样,生产者进程再次写缓冲区时,也能正确定位删除了一个数字的缓冲区的写位置。
终端也是临界资源
用printf()向终端输出信息是很自然的事情,但当多个进程同时输出时,终端也成为了临界资源,需要做好互斥保护,否则输出的信息可能错乱。
另外,printf()之后,信息只是保存在输出缓冲区内,还没有真正送到终端上,这也可能造成输出信息时序不一致。用fflush(stdout)可以确保数据送到终端。
应用程序的实现代码如下:
1 #define __LIBRARY__ 2 #include <unistd.h> 3 4 #include <stdio.h> 5 #include <linux/sem.h> 6 #include <sys/types.h> 7 #include <sys/stat.h> 8 #include <fcntl.h> 9 10 11 _syscall2(int,sem_open,const char*,name,unsigned int,vaule) 12 _syscall1(int,sem_wait,sem_t *,sem) 13 _syscall1(int,sem_post,sem_t *,sem) 14 _syscall1(int,sem_unlink,const char *,name) 15 16 #define BUFFER_LEN 10 17 #define M 60 18 #define CONSUMER1 20 19 #define CONSUMER2 20 20 #define CONSUMER3 20 21 22 /* 23 * producer---father process 24 * consumer1---son1 process 25 * consumer2---son2 processs 26 * consumer3---son3 process 27 */ 28 29 int main(void) 30 { 31 pid_t father,producer,consumer1,consumer2,consumer3,tmp1,tmp2,tmp3; 32 sem_t *p_empty_buf; 33 sem_t *p_full_buf; 34 sem_t *p_mutex; 35 int fd; 36 int data[10]; 37 int pos=0; 38 int num=0; 39 int a,b,c; 40 int i; 41 p_empty_buf=(sem_t *)sem_open("Empty",BUFFER_LEN); 42 p_full_buf=(sem_t *)sem_open("Full",0); 43 p_mutex=(sem_t *)sem_open("Mutex",1); 44 45 if( (fd=open("./buffer.txt",O_RDWR|O_CREAT|O_TRUNC,0644)) <0 ){ 46 printf("open error\n"); 47 exit(-1); 48 } 49 else{ 50 printf("open success\n"); 51 } 52 tmp1=fork(); 53 if(tmp1==0) /* son1---consumer1 */ 54 { 55 consumer1=getpid(); 56 printf("consumer1 is runing!\n"); 57 for(a=0;a<CONSUMER1;a++) 58 { 59 sem_wait(p_full_buf); 60 sem_wait(p_mutex); 61 if( (pos=lseek(fd,0,SEEK_CUR)) == -1){ 62 printf("seek pos error\n"); 63 } 64 if( (num = lseek(fd,0,SEEK_SET)) == -1){ 65 printf("lseek error\n"); 66 } 67 if( (num=read(fd,data,sizeof(int)*10)) == -1){ 68 printf("read error\n"); 69 } 70 else{ 71 printf("%d: %d\n",consumer1,data[0]); 72 } 73 fflush(stdout); 74 if( (num = lseek(fd,0,SEEK_SET))== -1){ 75 printf("lseek error\n"); 76 } 77 if( (num = write(fd,&data[1],sizeof(int)*9)) == -1){ 78 printf("read and write error\n"); 79 } 80 if( (num = lseek(fd,pos-sizeof(int),SEEK_SET))== -1){ 81 printf("lseek error\n"); 82 } 83 sem_post(p_mutex); 84 sem_post(p_empty_buf); 85 } 86 printf("Son1 is finished!\n"); 87 88 } 89 else if(tmp1>0) 90 { 91 tmp2 = fork(); 92 if(tmp2==0) 93 { 94 consumer2=getpid(); 95 printf("consumer2 is runing!\n"); 96 for(b=0;b<CONSUMER2;b++) 97 { 98 sem_wait(p_full_buf); 99 sem_wait(p_mutex); 100 if( (pos=lseek(fd,0,SEEK_CUR)) == -1){ 101 printf("seek pos error\n"); 102 } 103 if( (num = lseek(fd,0,SEEK_SET)) == -1){ 104 printf("lseek error\n"); 105 } 106 if( (num=read(fd,data,sizeof(int)*10)) == -1){ 107 printf("read error\n"); 108 } 109 else{ 110 printf("%d: %d\n",consumer2,data[0]); 111 } 112 fflush(stdout); 113 if( (num = lseek(fd,0,SEEK_SET))== -1){ 114 printf("lseek error\n"); 115 } 116 if( (num = write(fd,&data[1],sizeof(int)*9)) == -1){ 117 printf("read and write error\n"); 118 } 119 if( (num = lseek(fd,pos-sizeof(int),SEEK_SET))== -1){ 120 printf("lseek error\n"); 121 } 122 sem_post(p_mutex); 123 sem_post(p_empty_buf); 124 } 125 printf("Son2 is finished!\n"); 126 } 127 else if(tmp2>0) 128 { 129 tmp3 =fork(); 130 if(tmp3 == 0) 131 { 132 consumer3 = getpid(); 133 printf("consumer3 is runing!\n"); 134 for(c=0;c<CONSUMER3;c++) 135 { 136 sem_wait(p_full_buf); 137 sem_wait(p_mutex); 138 if( (pos=lseek(fd,0,SEEK_CUR)) == -1){ 139 printf("seek pos error\n"); 140 } 141 if( (num = lseek(fd,0,SEEK_SET)) == -1){ 142 printf("lseek error\n"); 143 } 144 if( (num=read(fd,data,sizeof(int)*10)) == -1){ 145 printf("read error\n"); 146 } 147 else{ 148 printf("%d: %d\n",consumer3,data[0]); 149 } 150 fflush(stdout); 151 if( (num = lseek(fd,0,SEEK_SET))== -1){ 152 printf("lseek error\n"); 153 } 154 if( (num = write(fd,&data[1],sizeof(int)*9)) == -1){ 155 printf("read and write error\n"); 156 } 157 if( (num = lseek(fd,pos-sizeof(int),SEEK_SET))== -1){ 158 printf("lseek error\n"); 159 } 160 sem_post(p_mutex); 161 sem_post(p_empty_buf); 162 } 163 printf("Son3 is finished!\n"); 164 } 165 else if(tmp3>0) 166 { 167 producer=getpid(); 168 printf("producer is runing!\n"); 169 for(i=0;i<M;i++) 170 { 171 sem_wait(p_empty_buf); /* P(empty) */ 172 sem_wait(p_mutex); /* P(mutex) */ 173 if( (num=write(fd,&i,sizeof(int))) == -1){ 174 printf("write error\n"); 175 } 176 /*else{ 177 printf("write in buffer %d\n",i); 178 }*/ 179 sem_post(p_mutex); /* V(mutex) */ 180 sem_post(p_full_buf); /* V(full) */ 181 } 182 wait((int *)NULL); 183 wait((int *)NULL); 184 wait((int *)NULL); 185 close(fd); 186 sem_unlink("Empty"); 187 sem_unlink("Full"); 188 sem_unlink("Mutex"); 189 printf("The father is finished\n"); 190 } 191 else 192 printf("Creat son3 failed\n"); 193 } 194 else 195 printf("Creat son2 failed\n"); 196 } 197 else 198 printf("Creat son1 failed\n"); 199 return 0; 200 }
标签:
原文地址:http://www.cnblogs.com/Wangzhike/p/4646279.html