4 用户态和内核态信号量-实验2:使用POSIX信号量实现进程同步

一.实验目的

·掌握在线程同步问题中POSIX无名信号量和有名信号量的使用方法。
·理解POSIX无名信号量和有名信号量的差异。

二.实验背景

·什么是信号量
操作系统教科书中信号量的定义,信号量是包含一个整型变量S并且带有两个原子操作wait 和 signal 的抽象数据类型。

·wait 操作也被称为 down、P和 lock。调用wait 操作时,如果信号量的整型变量S>0,wait 就将S减1,调用wait 的进程或则线程继续运行;如果S <0,wait 将S减1后把调用进程或线程挂起在信号的等待队列上。

·signal 操作也被称为 up、V或unlock。调用signal 操作时,signal 将S加1;如果S加1后的数值任然大于0,说明用进程或线程挂起在信号量的等待队列上,处于信号量等待队列队首的线程将被唤醒,使其从wait 中返回。

·进程和线程的差异
进程概念
线程概念
多个进程之间可能存在父子关系或兄弟关系,但也可能完全无关;
隶属于同一个进程的线程之间,一定存在相关性,
同步问题,包含狭义的同步与互斥,教材P.202
区分同步与互斥

·Recall: 广义的同步又可以分为狭义的同步和互斥,定义如下:
互斥:一组并发进程中的一个或多个程序段,因共享某一公有资源而导致它们必须以一个不允许交叉执行的单位执行。
同步(狭义):异步环境下的一组并发进程,因直接制约而互相发送消息而进行相互合作、互相等待,使得各进程按一定的速度执行的过程称为进程间的同步

·单向同步问题模型 教材P.210

semaphore notempty=0;//声明信号量notempty,初值为0,表示没有产品 producer() { /* 生产产品 */ ...... signal(notempty);//设置缓冲区非空的信号量 } consumer() { wait(notempty); // 等待缓冲区非空的通知 /* 消费产品*/ ....... }

·双向同步问题模型 教材P.211

semaphore notfull=1, notempty=0;//声明信号量 producer() { wait(notfull) // 等待缓冲区非满的通知 /* 生产产品 */ ...... signal(notempty); //设置缓冲区非空的信号量 } consumer() { wait(notempty);// 等待缓冲区非空的通知 /* 消费产品*/ ....... signal(notfull); //设置缓冲区非满的信号量 }

·同步与互斥相结合 教材P.211:一个生产者对N个消费者

semaphore notfull=1, notempty=0,lockc=1;//声明信号量 producer() { wait(notfull) // 等待缓冲区非满的通知 /* 生产产品 */ ...... signal(notempty); //设置缓冲区非空的信号量 } consumer() { wait(notempty); // 等待缓冲区非空的通知 wait(lockc); // 对产品消费行为加锁 /* 消费产品*/ ....... signal(lockc); // 消费完成后解锁 signal(notfull); //设置缓冲区非满的信号量 }

·同步与互斥相结合 教材P.212:M个生产者对N个消费者

semaphore notfull=1, notempty=0,lockc=1, lockp=1;//声明信号量 producer() { wait(notfull) // 等待缓冲区非满的通知 wait(lockp); // 对生产行为加锁 /* 生产产品 */ ...... signal(lockp); // 生产完成后解锁 signal(notempty); //设置缓冲区非空的信号量 } consumer() { wait(notempty);// 等待缓冲区非空的通知 wait(lockc); // 对产品消费行为加锁 /* 消费产品*/ ....... signal(lockc); // 消费完成后解锁 signal(notfull);//设置缓冲区非满的信号量 }

.一个互斥信号量的wait和signal操作一般总是在一个线程或进程中成对出现。与之相对,一个(狭义)同步信号量的wait和signal操作总是出现在两个不同的线程或进程中。原因是,(狭义)同步调节的是两个不同线程或进程之间的行为关系。

·互斥问题是以资源为核心,其协调的是多个进程(线程)和资源之间的使用关系;而同步问题是以事件或者消息为核心,协调的是进程(线程)和进程(线程)之间的行为顺序关系。

·在互斥问题中一个信号量的初值代表的是该类资源的数量;而在同步问题中信号量代表的是事件或消息的初始状态。
同类进程(线程)多数是互斥关系,而同步关系往往发生在不同类的进程(线程)间。

·POSIX信号量的定义,教材P.212

4 用户态和内核态信号量-实验2:使用POSIX信号量实现进程同步_#include

·无名信号量:
象变量一样声明
使用时需要进行初始化和销毁

int sem_init(sem_t *sem, int pshared,unsigned value); int sem_destroy(sem_t *sem);

与操作系统教科书中的waiti和signal对应的操作函数为

int sem_wait(sem_t *sem); int sem_post(sem_t *sem);

·有名信号量:
需要创建,创建时赋予一个系统级的名字

sem_t *sem_open(const char *name,int oflag,mode_t mode,unsigned int value);

使用完毕后需要进行关闭,然后销毁

int sem_close(sem_t *sem); int sem_unlink(const char *name);

与操作系统教科书中的waiti和signal对应的操作函数为

int sem_wait(sem_t *sem); int sem_post(sem_t *sem);

4 用户态和内核态信号量-实验2:使用POSIX信号量实现进程同步_互斥_02

三.关键代码及分析

1.无名信号量用于进程间同步的分析

#include <stdio.h>  
#include <sys/types.h>
#include <unistd.h>
#include <semaphore.h>

int main(void)
{ char buf[128];
sem_t sem_id;
int val;
sem_init(&sem_id,1,1); //第二个参数为1
if(fork()==0){
while(1){
sem_wait(&sem_id); //行A,此行注释掉、实现两个进程狭义同步
sleep(2);
sem_getvalue(&sem_id,&val);
sprintf(buf,"this is child,the sem value is %d",val);
printf("%s\n",buf);
sem_post(&sem_id);
}
}
while(1){
sem_wait(&sem_id);
sem_getvalue(&sem_id,&val);
sprintf(buf,"this is fahter,the sem value is %d",val);
printf("%s\n",buf);
sleep(2);
sem_post(&sem_id); //行B,此行注释掉、实现两个进程狭义同步
}
return 0;
}

该程序在调用sem_init 函数时第二个参数设为1,表示该信号量可在进程之间共享。然后在fork函数调用后,父、子进程分别各自执行使用了加锁和解锁的代码,实现进程间的互斥。但实际上该程序存在很隐蔽的错误。该程序使用无名信号量的方法违反了前面讲到的无名信号量的使用规则:无名信号量在使用时必须是多个进程(线程)的共享变量,无名信号量要保护的变量也必须是多个进程(线程)的共享变量。该程序虽然编译后运行时,确实会各自输出信息。运行结果好像是信号量sem在父、子进程间起到了互斥作用,但实际上这是父、子各自独立运行得到的结果,即使没有sem 信号量的保护也会按此顺序执行。

2.无名信号量用于相关进程间的同步

#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <semaphore.h>
#include <sys/mman.h>
#define COUNTNO 20
sem_t sem;
sem_t *psem = NULL;
int main(int argc, char *argv[])
{
int pid,i;
int val;
//内存映射实现信号量共享
psem = (sem_t *)mmap(NULL, sizeof(sem_t), PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANONYMOUS, -1, 0); //行C
if (psem == NULL)
{
printf("Share memory failed!\n");
return -1;
}
if (sem_init(psem, 1, 1) == -1)
{
printf("Init unamed sem failed!\n");
return -1;
}
sem_getvalue(psem,&val);
printf("this is the main function the psem value is %d\n",val);

pid = fork();
if(pid==0){
for (i=1;i<COUNTNO;i++)
{
sem_wait(psem); //行A ,此行注释掉实现两个进程狭义同步
sem_getvalue(psem,&val);
printf("this is child,the sem value is %d\n",val);
sem_post(psem);
usleep(1000);
}
}
else {
for (i=1;i<COUNTNO;i++)
{
sem_wait(psem); //行B,此行注释掉实现两个进程狭义同步
sem_getvalue(psem,&val);
printf("this is fahter,the sem value is %d\n",val);
sem_post(psem);
usleep(3000);
}
}
sem_destroy(psem);
return 0;
}
3.有名信号量用于相关进程间的同步
#include <stdlib.h>
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <semaphore.h>
#include <fcntl.h>
#include <errno.h>
//创建模式权限
#define FILE_MODE (S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)
#define SEM_FORK "fork" /* 信号量fork */

int main(int argc,char **argv)
{
sem_t *sem;
int val,i;


if((sem = sem_open(SEM_FORK,O_CREAT,FILE_MODE,0)) == SEM_FAILED) //用于同步
{
printf("sem_open %s Error\n",argv[0]);
exit(-1);
}


if(fork()==0){
for (i=1;i<5;i++)
{
sleep(1);
printf("this is child\n");
sem_post(sem); //A:信号量post
}
sem_close(sem);
}
else
{
for (i=1;i<5;i++)
{
sem_wait(sem); //B:信号量wait
sleep(1);
printf("this is father\n");
}
sem_close(sem);
}
sem_unlink(SEM_FORK);
return 0;
}
#include <stdlib.h>
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <semaphore.h>
#include <fcntl.h>
#include <errno.h>
//创建模式权限
#define FILE_MODE (S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)
#define SEM_FORK "fork" /* 信号量fork */

int main(int argc,char **argv)
{
sem_t *sem;
int val,i;


//if((sem = sem_open(SEM_FORK,O_CREAT,FILE_MODE,0)) == SEM_FAILED) //用于同步
if((sem = sem_open(SEM_FORK,O_CREAT,FILE_MODE,1)) == SEM_FAILED) //用于互斥
{
printf("sem_open %s Error\n",argv[0]);
exit(-1);
}


if(fork()==0){
for (i=1;i<5;i++)
{
sem_wait(sem);
sleep(1);
printf("this is child\n");
sem_post(sem);
}
sem_close(sem);
}
else
{
for (i=1;i<5;i++)
{
sem_wait(sem);
sleep(1);
printf("this is father\n");
sem_post(sem);
}
sem_close(sem);
}
sem_unlink(SEM_FORK);
return 0;
}

4.有名信号量用于无关进程间的同步

生产者向一个文件写人消息消费者从该文件中读取消息;当消费者进程运行时如果没有接到通知就不从文件读取消息,并阻塞自己,直到接到生产者的消息才被唤醒读取文件中的信息并显示出来。该例中生产者进程对消费者进程实现了单向同步制约。

#include <stdlib.h>
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <semaphore.h>
#include <fcntl.h>
#include <errno.h>
//创建模式权限
#define FILE_MODE (S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)
#define SEM_INFO "info" /* 信号量info用于消息通知 */

int main(int argc,char **argv)
{
sem_t *info; /* 信号量info用于传递消息*/
int fd,valp;
char buf[128];

//创建NAMED信号量
if((info = sem_open(SEM_INFO,O_CREAT,FILE_MODE,0)) == SEM_FAILED)
{
printf("sem_open %s Error\n",argv[0]);
exit(-1);
}
if((fd=open("test", O_WRONLY|O_CREAT))==-1)
{
printf("Open %s Error\n",argv[0]);
exit(-1);
}
sprintf(buf,"this info is from producer %d",getpid());
write(fd, buf, 128);
sem_post(info);
sem_getvalue(info,&valp);
printf("semaphore %s has created by producer %d\n",SEM_INFO,getpid());
printf("pro:the sem value is %d\n",valp);
close(fd);
sem_close(info);
//sem_unlink(SEM_INFO);
return 0;
}
#include <stdlib.h>
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <semaphore.h>
#include <fcntl.h>
#include <errno.h>
//创建模式权限
#define FILE_MODE (S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)
#define SEM_INFO "info" /* 信号量info用于消息通知 */
#define BUFFER_SIZE 512
char buffer[BUFFER_SIZE];
int main(int argc,char **argv)
{
sem_t *info; /* 信号量info用于传递消息*/
int fd,valp;
int len;
//创建NAMED信号量
if((info = sem_open(SEM_INFO,O_CREAT,FILE_MODE,0)) == SEM_FAILED)
//if((info = sem_open(SEM_INFO,0)) == SEM_FAILED)
{
perror("sem_open() error");
exit(-1);
}
sem_getvalue(info,&valp);
printf("csm:the sem value is %d\n",valp);
sem_wait(info);
if((fd=open("test", O_RDONLY))==-1)
{
printf("Open %s Error\n",argv[0]);
exit(-1);
}
len = read(fd, buffer, 512);
printf("this is consumer %d --->>>",getpid());
printf("%s\n",buffer);
close(fd);
sem_close(info);
//sem_unlink(SEM_INFO);

return 0;
}

四.实验结果与分析

1.无名信号量用于进程间的同步的分析

·gcc -o fork_semu2 fork_semu2.c -lpthread //编译连接生成可执行程序

4 用户态和内核态信号量-实验2:使用POSIX信号量实现进程同步_操作系统_03


·./fork_semu2 //执行生产的可执行程序

4 用户态和内核态信号量-实验2:使用POSIX信号量实现进程同步_互斥_04

但这种在进程同使用无名信号量的方法存在根本性的错误。其原因在于,虽然在sem_init函数中指定了信号量sem是在父、子进程间共享的,但实际上真正运行时父、子进程各自拥有一个sem信号号量,要验证上述错误并不难,只要把上述代码中的行A和行B注释掉,重新编译、执行,就会发现父进程只运行一次,以后即不再运行;而子进程则一直在运行,并且信号量的值在不断增加。其原因就在于在上述代码中,虽然指定了信号量是在父、子进程同共享,但实际上由于fork函数的作用,相当于在父、子进程之间各自创建了一个名为sem的信号量,也因此注释掉行A和行B后,虽然子进程不断通过sem_post发出sem信号量可用的通知,但实际上由于父进程使用的是自己进程空间的另个sem,所以父进程一直被阻塞不能运行。结果只是子进程的信号量值不断增加,但父进程仍被阻室。

4 用户态和内核态信号量-实验2:使用POSIX信号量实现进程同步_互斥_05


由上面的分析可知,在调用 fork函教的父、子进程之间使用无名信号量也必须将其放置在父、子进程的共享内存区城内,单纯地把setm_init 的共享参数设为1是不够的。

2.无名信号量用于相关进程间的同步

·gcc -o shm_usem03 shm_usem03.c -lpthread //编译连接生成可执行程序

4 用户态和内核态信号量-实验2:使用POSIX信号量实现进程同步_linux_06


· ./shm_usem03 //执行生产的可执行程序

4 用户态和内核态信号量-实验2:使用POSIX信号量实现进程同步_操作系统_07


·注释掉行A、B后:

4 用户态和内核态信号量-实验2:使用POSIX信号量实现进程同步_信号量_08

在上述程序中行 psem = (sem_t *)mmap(NULL, sizeof(sem_t), PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANONYMOUS, -1, 0);的功能是把一个信号量 sem映射到共享内存中,其指针为psem,在完成该映射后,父、子进程将使用同一个信号量实现进程间的同步。可以测试把行A和行B注释去掉后程序此时信号量的值,信号量的值随父、于进程分别调用sem_wait 和sem_post 而在不断地改变,表明二者使用的是同一个信号量。另外,之所以在父.子进程中使用uslep雨数是为了增加进程之间并发的随机性.
综上所述,POSIX无名信号量用于进程间的同步时,必须要确保:无名信号量以及无名信号量要保护的变量必须是多个进程(线程)的共享变量。当然如果仅仅狭义同步,那么可能不需要信号量要保护的变量,

3.有名信号量用于相关进程间的同步

·gcc -o fork_semn1 fork_semn1.c -lpthread //编译连接生成可执行程序

4 用户态和内核态信号量-实验2:使用POSIX信号量实现进程同步_linux_09


· ./fork_semn1 //执行生产的可执行程序

4 用户态和内核态信号量-实验2:使用POSIX信号量实现进程同步_互斥_10

fork创建的子进程和父进程通过有名信号量fork实现了子进程对父进爱的单向同步制约。可以在程序运行期间观察/dev/shm/目录下sem. fork文件的创建在进程退出时该文件随着sem_unlink 雨数的调用而被删除。

·gcc -o fork_semn2 fork_semn2.c -lpthread //编译连接生成可执行程序

4 用户态和内核态信号量-实验2:使用POSIX信号量实现进程同步_#include_11


· ./fork_semn2 //执行生产的可执行程序

4 用户态和内核态信号量-实验2:使用POSIX信号量实现进程同步_互斥_12

3.有名信号量用于相关进程间的同步

· gcc -o pro_namedsem pro_namedsem.c -lpthread
//编译连接生成可执行程序
· gcc -o csm_namedsem csm_namedsem.c -lpthread

4 用户态和内核态信号量-实验2:使用POSIX信号量实现进程同步_#include_13

· ./pro_namedsem //执行生产的可执行程序

首先运行生产者进程pro_namedsem,再运行消费者进程csm_namedsern,从文件中读取了生产者写人的信息。而如果先在后台运行消费者进程,则消费者进程会被阻塞在信号量info上,直到生产者生产进程调用sem_post,消费者才会被唤醒

4 用户态和内核态信号量-实验2:使用POSIX信号量实现进程同步_操作系统_14

sem_unlink语句用于删除创建在/dev/shm 目录下的sem.info信号量文件,此时信号量info仍然存在,在程序结果后该信号量info自动被撒销,所有这些资料中,特别是在q嵌人式系统中,可能会在调用scm_open后立即调用sem_unliak利除掉/dev/ shm目录下的信号量文件。如果不调用sem_unink 语句,则/dev/ shm目录下创建的相应的信号量文件会在系统内核重启后自动删除。考虑到生产者和消费者进程的异步特性,所以并未掉用sem_unlink 语句,以便在测试时生产者和消费者之间传递的信号量不会丢失。