操作系统学习笔记(5)——信号量与P/V操作与进程通信
1、信号量与P/V操作
- 信号量的构成
1 Struct semaphore { 2 int value; //信号量值 3 pointer_PCB queue; //信号量队列指针 4 }
-
- 信号量是联系和控制CR(需求的资源)的数据结构。
- 当信号量的值>0时,s.value表示CR的可用数目
- 当信号量的值=0时,s.value表示无空闲CR
- 当信号量的值<0时,|s.value|表示因CR而阻塞的进程数
- P(Proberen)操作
1 P(s) { 2 s.value = s.value - 1; // s.value减1 3 if (s.value < 0) // 该进程被阻塞,进入相应队列,然后转进程调度 4 { 5 该进程状态置为等待状态; 6 将该进程加入相应的等待队列s.queue的末尾; 7 } 8 //若s.value减1后仍大于或等于0,则进程继续执行 9 }
- V(Verhogen)操作
1 V(s) { 2 s.value = s.value + 1; // s.value加1 3 if (s.value <= 0) // 从队列中唤醒一等待进程,然后继续执行或转进程调度 4 { 5 唤醒相应等待队列s.queue中等待的一个进程; 6 将其状态修改为就绪态,并将其插入就绪队列; 7 } 8 //若相加结果大于0,进程继续执行 9 }
- 利用信号量和P/V操作实现进程互斥的一般模型:
- 进程P1 进程P2
- ...... ......
- P(S); P(S);
- 临界区; 临界区;
- V(S); V(S);
- ...... ......
- 其中信号量S用于互斥,初值为1,当P1进程进入临界区后S的值就为0,P2执行P(S)后进入阻塞队列从而实现了对临界区的互斥访问。
- 使用P/V操作实现进程互斥的注意点
- 每个程序中用户实现互斥的P、V操作必须成对出现,先做P操作,进临界区,后做V操作,出临界区。若有多个分支,要认真检查其成对性;
- P、V操作应分别紧靠临界区的头尾部,临界区的代码应尽可能短,不能有死循环;
- 互斥信号量的初值一般为1。
- 利用信号量和P/V操作实现进程同步的一般模型:
- S:=0
- 进程P1 进程P2
- V(S); P(S);
- ...... ......
- s初值为0,就意味着只能先做v操作,就是先执行p1,然后才能执行p2里的p操作,所以实现了p1 和p2的先后次序同步关系
- 使用P/V操作实现进程同步的注意点
- 分析进程间的制约关系,确定信号量种类。在保持进程间有正确的同步关系情况下,哪个进程先执行,哪些进程后执行,彼此间通过什么资源(信号量)进行协调,从而明确要设置哪些信号量;
- 信号量的初值与相应资源的数量有关,也与P、V操作在程序代码中出现的位置有关;
- 同一信号量的P、V操作要成对出现,但它们分别在不同的进程代码中。
- 生产者消费者经典问题分析
- 一个生产者,一个消费者,公用一个缓冲区
- 首先两进程互斥进入缓冲区,然后是需要同步,先生产再消费
- 但也需要考虑存在的另一种同步,即当缓冲区满的时候,是先消费再生产
- 一个生产者,一个消费者,公用n个环形缓冲区
- 同步和互斥关系同上
- 与第一种情况的区别在于需要设缓冲区的编号为1~n-1,定义两个指针in和out,分别是生产者进程和消费者进程使用的指向下一个可用的缓冲区
- 一组生产者,一组消费者,公用n个环形缓冲区
- 互斥关系:消费者和消费者、消费者和生产者、生产者和生产者。
- 同步关系:缓冲区为空,先生产再消费;缓冲区为满,先消费再生产。
- 每一对同步关系设置一个同步信号量,题目中存在两对同步关系,就设置两个同步信号量 empty和full。需要注意同步信号量初值不一定为0,只要根据同步关系保证先后执行次序就可以了。
- 对同一资源使用的一组进程可以设置一个互斥信号量,互斥信号量初值一般为1
- 定义四个信号量:
- empty——表示缓冲区是否为空,初值为n
- full——表示缓冲区中是否为满,初值为0
- mutex1——生产者之间的互斥信号量,初值为1
- mutex2——消费者之间的互斥信号量,初值为1
- 为了同步生产者和消费者的行为,需要记录缓冲区中物品的数量。数量可以使用信号量来进行统计,这里需要使用两个信号量:empty 记录空缓冲区的数量,full 记录满缓冲区的数量。其中,empty 信号量是在生产者进程中使用,当 empty 不为 0 时,生产者才可以放入物品;full 信号量是在消费者进程中使用,当 full 信号量不为 0 时,消费者才可以取走物品。
- 由于需要设缓冲区的编号为1~n-1,所以定义两个指针in和out,分别是生产者进程和消费者进程使用的指向下一个可用的缓冲区。
- 生产者进程:
- 一个生产者,一个消费者,公用一个缓冲区
1 while(TRUE){ 2 生产一个产品; 3 P(empty); 4 P(mutex1); 5 产品送往buffer(in); 6 in=(in+1)mod n; 7 V(mutex1); 8 V(full); 9 }
-
-
- 消费者进程:
-
1 while(TRUE){ 2 P(full) 3 P(mutex2); 4 从buffer(out)中取出产品; 5 out=(out+1)mod n; 6 V(mutex2); 7 V(empty); 8 消费该产品; 9 }
-
-
- 需要注意,不能先对缓冲区进行加锁,再测试信号量。也就是说,不能先执行 p(mutex) 再执行 p(empty)。如果这么做了,那么可能会出现这种情况:生产者对缓冲区加锁后,执行 p(empty) 操作,发现 empty = 0,也就是没有空的缓冲期可以放产品,此时生产者只能阻塞;但因为mutex的值已经为0,所以消费者不能进入临界区,消费者就永远无法执行 p(empty) 操作,empty 永远都为 0,导致生产者永远等待下,不会释放锁,消费者因此也会永远等待下去。但两个v操作顺序任意,不影响。
- 3个进程PA,PB和PC合作解决⽂件打印问题
- 问题描述
- PA将⽂件记录从磁盘读⼊主存的缓冲区1,每执⾏⼀次读⼀个记录;
- PB将缓冲区1的内容复制到缓冲区2,每执⾏⼀次复制⼀个记录;
- PC将缓冲区2的内容打印出来,每执⾏⼀次打印⼀个记录。缓冲区的⼤⼩等于⼀个记录⼤⼩。 请⽤P,V操作来保证⽂件的正确打印。
- 问题描述
-
-
-
- 这是典型的双缓冲区问题,两个缓冲区⼤⼩均为1,其中PA为⽣产者,PB既是⽣产者又是消费者,PC为消费者, 故按照⽣产消费者问题的思路解决该题即可。但是需要注意的问题是:如果PA将数据放⼊缓冲区之后,PB没有及时取的话,如果此时PA进程继续执⾏,那么会将之前的数据覆盖掉,缓冲区2也⼀样,因此这⾥还需要设置两个 信号量以限制缓冲区1和缓冲区2中记录的数量。
- 设置信号量
- full1,full2分别表⽰缓冲区1及缓冲区2是否有记录可供处理,初值均为0;
- empty1,empty2分别表⽰缓冲区1及缓冲区2是否还有空位可放记录,初值均为1;
- mutex1,mutex2分别表⽰缓冲区1及缓冲区2的访问控制,初值均为1。
-
1 semaphore full1 = 0; 2 semaphore full2 = 0; 3 semaphore empty1 = 1; 4 semaphore empty2 = 1; 5 semaphore mutex1 = 1; 6 semaphore mutex2 = 1; 7 8 PA() { 9 while (1) { 10 从磁盘读⼀一个记录; 11 P(empty1); 12 P(mutex1); 13 将记录存⼊入缓冲区1; 14 V(mutex1); 15 V(full1); 16 } 17 } 18 19 PB() { 20 while(1) { 21 P(full1); 22 P(mutex1); 23 从缓冲区1中读出⼀一个⽂文件记录; 24 V(mutex1); 25 V(empty1); 26 P(empty2); 27 P(mutex2); 28 将⼀一个记录读⼊入缓冲区2; 29 V(mutex2); 30 V(full2); 31 } 32 } 33 34 PC() { 35 while (1) { 36 P(full2); 37 P(mutex2); 38 从缓冲区2取⼀一个记录; 39 V(mutex2); 40 V(empty2); 41 打印记录; 42 } 43 }
-
- 读者写者问题
- 问题描述
- 允许多个读者同时对文件进行读操作
- 只允许一个写者对文件进行写操作
- 任何写者在完成写操作前不允许其他读者或写者工作
- 写者在执行写操作前,应让已有的写者和读者全部退出
- 分析关系:读与读不互斥,读与写互斥,写与写互斥
- 读者分类:
- 第一个进入的读者:打开写屏蔽,读入
- 中间进入的读者:读入
- 最后一个离开的读者:关闭写屏蔽,离开
- 为了区分三类读者就必须引入计数器rc对读进程计数,即做加1或减1操作。但大家都知道加1和减1操作在机器内部是三条语句完成的,为了不引起计数器的混乱,就必须对加减定义成原子操作,即需加1或减1一次性完成,mutex就是用于对计数器rc操作的互斥信号量,w则表示是否允许写的信号量,也就是写屏蔽开关
- 问题描述
- 读者写者问题
1 void reader(void) 2 { 3 while(TRUE) 4 { 5 P(mutex); 6 rc = rc + 1; 7 if(rc == 1) /*第一个读者*/ 8 { 9 P(W); /*不需要每给读者都做*/ 10 } 11 V(mutex); 12 读操作; 13 P(mutex); 14 rc = rc - 1; 15 if (rc == 0)/*最后一个读者*/ 16 { 17 V(w); 18 } 19 V(mutex); 20 其他操作; 21 } 22 } 23 24 void writer(void) 25 { 26 while(TRUE) 27 { 28 ...... 29 P(W); 30 写操作; 31 V(w); 32 ...... 33 } 34 }
2、进程通信概念和类型
- 基本概念
- 进程间通信IPC是指进程在系统里同时运行,并相互传递、交换信息
- 通过进程通信能够实现数据传输、共享数据、通知事件、资源共享、进程控制
- 进程通过与内核及其他进程之间的互相通信来协调它们的行为
- 通信类型
- 低级通信
- 高级通信
- 通信方式
- 数据格式
- 字节格式:接收方不保留各次发送之间的分界
- 报文格式:接收方保留歌词发送之间的分界;分为定长报文/不定长报文和可靠报文/不可靠报文
- 同步方式
- 阻塞操作:指操作方要等待操作结束
- 不阻塞操作:指操作提交后立即返回
- 数据格式
3、低级通信中的信号通信
- 机制原理
- 每个信号都要对应正整数常量,即信号编码;
- 进程之间传送事先约定的信息的类型,用于通知进程发生了某异常事件;
- 进程通过信号机制来检查是否有信号。若有,中断正在执行的程序,转向的对应处理程序;结束后返回到断点继续执行,这是一种软中断。
- 信号收发
- 发送信号,发送信号的程序用系统调用kill( )实现;
- 预置信号处理,接收信号的程序用signal( )来实现对处理方式的调用;
- 接收信号的进程按事先规定完成对事件的处理。
- kill( )
- int kill(pid, sig)
- pid是进程的标识符,参数sig是要发送的软中断信号
- pid>0时,信号发送给进程pid
- pid=0时,信号发送给与发送进程同组的所有进程
- pid=-1时,信号发送给所有用户标识符真正等于发送进程的有效用户标识号的进程
- signal( )
- signal(sig, function)
- 头文件 #include<signal.h>
- 参数定义 int sig; void (*func)( );
- function的解释如下
- function=1时,对信号不予理睬,屏蔽该类信号
- function=0时,进程在收到sig信号后终止自己
- function≠0且≠1时,值作为信号处理程序的指针
- 代码实例
1 #include<stdio.h> 2 #include<signal.h> 3 #include<unistd.h> 4 5 int wait_mark; 6 void waiting() { 7 while (wait_mark != 0); 8 } 9 void stop() { 10 wait_mark = 0; 11 } 12 void main() { 13 int p1, p2; 14 signal(SIGINT, SIG_IGN); //防止control-C键盘中断 15 while ((p1 = fork()) == -1); //创建子进程p1 16 if (p1 > 0) { 17 while ((p2 = fork()) == -1); //创建子进程p2 18 if (p2 > 0) { 19 wait_mark = 1; 20 signal(SIGINT, stop); //接收到^c信号,转stop 21 waiting(); 22 kill(p1, 16); //向p1发软中断信号16 23 kill(p2, 17); //向p2发软中断信号17 24 wait(0); //同步 25 wait(0); 26 printf("Parent process is killed!\n"); 27 exit(0); 28 } 29 else { 30 wait_mark = 1; 31 signal(17, stop); //接收到软中断信号17,转stop 32 waiting(); 33 printf("Child process 2 is killed by parent!\n"); 34 exit(0); 35 } 36 } 37 else { 38 wait_mark = 1; 39 signal(16, stop); //接收到软中断信号17,转stop 40 waiting(); 41 printf("Child process 1 is killed by parent!\n"); 42 exit(0); 43 } 44 }
4、高级通信中的共享存储
- 机制原理
- 共享存储区是系统中通信速度最高的一种通信机制
- 进程通过对共享存储区中数据的读、写来进行通信
- 函数调用
- tshmget( )
- 创建、获得一个共享存储区
- 系统调用格式:shmid = shmget(key, size, flag);
- key是共享存储区的名字
- size是其大小
- flag是用户设置的标志
- shmat( )
- 共享存储区附接,将共享存储区附接进程虚拟地址空间
- 系统调用格式:virtaddr = shmat(shmid, addr, flag);
- shmid是共享存储区的标识符
- addr是用户给定,将共享存储区附接到进程的虚地址空间
- flag规定读、写权限,值为0时,表示可读、可写
- 返回值是共享存储区所附接到的进程虚拟地址virtaddr
- shmdt( )
- 把共享存储区从进程虚地址空间断开
- 系统调用格式:shmdt(addr);
- addr是要断开连接的虚地址,即shmat( )所返回的虚地址
- 调用成功,返回0值,调用不成功,返回-1
- shmctl( )
- 共享存储区的控制,对其状态进行读取和修改
- 系统调用格式:shmctl(shmid,cmd,buf);
- buf是用户缓冲区地址
- cmd是操作命令:用于查询共享存储区的情况,如长度、连接进程数、共享区的创建者标识符等;用于设置或改变共享存储区的属性,如共享存储器的许可权、连接进程计数等;共享存储区的加锁和解锁,删除共享存储区标识符等。
- tshmget( )
- 实例
- 进程利用fork( )创建两个子进程server和client进行通信;
- client端建立或打开 一个key为75的共享区,client端填入9到0,client每发送一次数据后显示“(client)sent”;
- server端建立或打开一个key为75的共享区,等待其他进程发来的消息,server每接收到一次数据后显示“(server)received”
1 #include<sys/types.h> 2 #include<sys/shm.h> 3 #include<sys/ipc.h> 4 #define SHMKEY 75 5 int shmid, i; 6 int *addr; 7 void server() { 8 int x; 9 shmid = shmget(SHMKEY, 1024, 0777|IPC_CREAT); //创建共享存储区 10 addr = shmat(shmid, 0, 0); //获得首地址 11 do { 12 *addr = -1; 13 while (*addr == -1); 14 x = *addr; 15 printf("(server)received"); 16 } while(*addr); 17 shmctl(shmid, IPC_RMID, 0); //撤销共享存储区,归还资源 18 exit(0); 19 } 20 void client() { 21 int i; 22 shmid = shmget(SHMKEY, 1024, 0777|IPC_CREAT); //打开共享存储区 23 addr = shmat(shmid, 0, 0); //获得共享存储区首地址 24 for (i = 9; i >= 0; i--) { 25 while(*addr != -1); 26 printf("(client)sent"); 27 *addr = i; 28 } 29 exit(0); 30 } 31 void main() { 32 while ((i = fork()) == -1); 33 if (!i) server(); 34 system("ipcs -m"); 35 while ((i = fork()) == -1); 36 if (!i) client(); 37 wait(0); 38 wait(0); 39 }
5、高级通信中的消息通信
- 机制原理
- 消息是一个格式化的可变长信息单元
- 消息通信机制允许由进程给其他进程发送消息
- 进程收到多个消息时,可排成消息队列
- 消息队列有消息队列描述符方便用户和系统访问
- 函数调用
- msgget( ):创建一个消息,获得消息的描述符
- msgsnd( ):向指定的消息队列发送一个消息,并将该消息链接到该消息队列的尾部
- msgrcv( ):从指定的消息队列中接收消息
- msgctl( ):读取消息队列的状态并进行修改,如查询消息队列描述符、修改许可权及删除该队列
6、高级通信中的管道通信
- 机制原理
- 管道是连接写进程和读进程的、并允许以生产者消费者方式进行通信的共享文件,称为pipe文件
- 由写进程从管道的写入端将数据写入管道,而读进程则从管道的读出端读出数据
- 管道类型
- 有名管道:在文件系统中长期存在的、具有路径名的文件,用系统调用mknod( )建立,其他进程可以利用路径名来访问该文件,与访问其他文件相似
- 无名管道:利用pipe( )建立起来临时的无名文件,用该系统调用返回的文件描述符来标识该文件,只有调用pipe( ) 的进程及其子孙进程才能识别此文件描述符并利用该管道进行通信