信号量互斥编程

在程序中利用信号量互斥来解决公示板问题

背景知识

1、信号灯的概念

信号灯,又被称为信号量(semaphore),是IPC(进程间通信)的方式之一。它可以用来保证两个或多个关键代码段不被多个进程并发调用。每个信号灯都有个semval,用于记录信号灯的值。在进入一个关键代码段之前,进程必须获取一个信号量,使semval减1;一旦该关键代码段完成了,那么该进程必须释放信号量,使semval加1.其它想进入该关键代码段的进程,如果semval是0,就必须等待直到第一个进程释放信号量。

2、信号灯的用法

使用共享内存进行进程间通讯一般会经历下面几步:

1)建立信号灯集:进程通过调用函数semget来创建或者获得一个信号灯集;

2)初始化semval:通过semctl使用SETVAL命令来初始化信号灯的值(semval);

3)获取与释放semval:通过函数semop来获取或者释放信号灯,其中获取对应于semval值减1,释放对应于semval值加1.

4)删除信号灯集:当进程结束使用信号灯时,使用semctl通过IPC_RMID命令来删除它。

3、函数参考表

semget:

函数功能:创建与打开信号灯集

头文件:#include<sys/types.h>

#include<sys/ipc.h>

#include<sys/sem.h>

函数原型:int semget(key_t key,int nsems,int semflg);

参数说明:key:表示信号灯集的键值。当key的取值为IPC_PRIVATE,则函数semget()将创建一个新的信号灯集。

nsems:表示创建的信号灯集中的信号灯个数。

semflg:表示的操作类型,也可用于设置信号灯的访问权限,两者通过'|'连接表示。

IPC_CREAT:当打开一个信号量时没有则会创建一个信号量。

返回值:成功,则返回信号灯的IPC标识符。失败,则返回-1,errno存储错误原因。

EACCESS:没有权限

EEXIST:信号灯集已经存在,无法创建。

EIDRM:信号灯集已经删除。

ENOENT:信号灯集不存在,同时没有使用IPC_CREAT.

ENOMEM:没有足够的内存创建新的信号灯集。

ENOSPC:超出限制。

ftok:

函数功能:通过将文件路径名和子序号,获得System V IPC 键值(即创建消息队列、共享内存所用的键值)

#include<sys/types.h>

#include<sys/ipc.h>

函数原型:key_t ftok(const char *pathname,int proj_id);

参数说明:pathname:指定的带路径的文件名。

proj_id:子序号id,或称工程id。

如指定文件的索引节点号为65538,,换算成16进制为0x010002,而指定的ID值为38,换算成16进制为0x26,则最后的key_t返回值为0x26010002。

返回值:成功,则返回System V IPC键值。失败,则返回-1,errno储存错误原因。

semop

函数功能:信号灯的操作函数

头文件:#include<sys/types.h>

#include<sys/ipc.h>

#include<sys/sem.h>

函数原型:int semop(int semid,struct sembuf *sops,unsigned nsops);

参数说明:semid:待操作的信号灯集ID。

sops:指向待操作的信号灯结构体(数组)。sembuf结构体包含了对于某个信号灯操作方法的信息,其成员如下:

unsigned short sem_num;//信号灯编号,从0开始

short sem_op;//为正时,代表释放的信号灯值,为负时代表获取是信号灯值

short sem_flg;//操作的标识

nsops:表示要操作的信号灯数。

返回值:成功时,这个函数返回共享内存的起始地址,失败时返回-1.

semctl

函数功能:信号灯的控制

头文件:#include<sys/types.h>

#include<sys/ipc.h>

#include<sys/sem.h>

函数原型:int semctl(int semid,int semnum,int cmd,union semun arg);

参数说明:

semid:信号灯集的ID.

semnum:操作的信号灯编号。

cmd:是控制命令,常用的命令有

IPC_RMID:将信号灯集从内存中删除。

GETPID:获得sempid

GETVAL:获得semval

SETVAL:设置semval

arg:是一个共同体类型的副本。其中各个量的使用情况和参数cmd的设置有关。

返回值:失败时返回-1,成功返回与cmd相关的正数,例如:

GETPID:返回sempid

SETVAL:返回semval

st1.c

st2.c

执行st1,进入睡眠

st2去获取信号量失败,则进行阻塞

当st1程序操作完成并释放了信号量之后,则st2重新获取信号量成功后,才得到运行。

查看board.txt文件

键值---key(有点类似于文件对应的文件描述符fd)

每一个进程通信(IPC)对象不管是信号量,消息队列还是共享内存都有一个键值(数字)与之对应,不管打开与否都会有一个键值与之对应,当打开该进程通信对象后会有一个标示符,本标示符与键值不一样的,标识符打开之后才有的。可以利用键值来找到进程通信对象。

指定键值:

1、任意指定一个数

缺点:这个数已经被别的IPC对象(消息队列,共享内存)所使用了,在与新创建的信号量关联时就会失败。

2、构造一个尽量不会被别的IPC对象用到的数

方法:使用key_t ftok(char *fname,int id)。

ftok的基本工作原理

ftok(文件名,项目id)

文件名:利用文件名可以得到一个数字,这个数字是取得文件在内核里面的保存文件信息的一个结构里面的数字(索引节点号)。

然后与项目id组合成相应的键值。

AT&T的贝尔实验室,对Unix早期的进程间通信进行了改进和扩充,形成了"system V IPC",其通信进程主要局限在单个计算机内。IPC对象指的是共享内存(share memory)、消息队列(message queue)和信号灯集(semaphore)。

信号灯(semaphore),也叫信号量。它是不同进程间或一个给定进程内部不同线程间同步的机制。System V的信号灯是一个或者多个信号灯的一个集合。其中的每一个都是单独的计数信号灯。System V 信号灯由内核维护。主要函数semget,semop,semctl。

本文重点介绍的是semop函数。该函数主要功能是对信号灯进行P/V操作。

P操作则把当前进程由运行状态转换为阻塞状态,直到另外一个进程唤醒它。操作为:申请一个空闲资源(把信号量减1),若成功,则退出;若失败,则该进程被阻塞;

V操作负责把一个被阻塞的进程唤醒,它有一个参数表,存放着等待被唤醒的进程信息。操作为:释放一个被占用的资源(把信号量加1),如果发现有被阻塞的进程,则选择一个唤醒之。

semop函数原型如下:

int semop(int semid, struct sembuf  *sops, unsigned nsops);

semop操作中:sembuf结构的sem_flg成员可以为0、IPC_NOWAIT、SEM_UNDO 。为SEM_UNDO时,它将使操作系统跟踪当前进程对这个信号量的修改情况,如果这个进程在没有释放该信号量的情况下终止,操作系统将自动释放该进程持有的。

sembuf结构的sem_flg成员为SEM_UNDO时,它将使操作系统跟踪当前进程对这个信号量的修改情况,如果这个进程在没有释放该信号量的情况下终止,操作系统将自动释放该进程持有的信号量

问题描述:假设父子进程对一个文件进行写操作,但是这个文件同一时间只能有一个进程进行写操作。

示例程序如下:

#include <stdio.h>
        //……此处省略了头文件
        void P(int sid)
        {
            struct sembuf sem_p;
            sem_p.sem_num = 0;
            sem_p.sem_op = -1;
            sem_p.sem_flg = 0;

            if (semop(sid, &sem_p, 1) == -1)
            {
                perror("p op failed");
                exit(1);
            }
        }

        void V(int sid)
        {
            struct sembuf sem_p;
            sem_p.sem_num = 0;
            sem_p.sem_op = 1;
            //sem_p.sem_flg = SEM_UNDO;
            sem_p.sem_flg = 0;

            if (semop(sid, &sem_p, 1) == -1)
            {
                perror("v op failed");
                exit(1);
            }
        }

        int main(int argc, char * argv[ ])
        {
            pid_t pid;
            int fd;
            key_t key;
            int sid;

            if ((fd = open("semset", O_RDWR | O_CREAT, 0666)) == -1)
            {
                perror("open");
                exit( -1);
            }

            if ((key=ftok("semset", 'a')) == -1)
            {
                perror("ftok");
                return -1;
            }

            if ((sid = semget(key, 1, IPC_CREAT | 0666)) == -1)
            {
                perror("createSemset");
                exit(-1);
            }

            if( -1==semctl(sid, 0, SETVAL, 1) )
            {
                perror("SETVAL");
                exit(1);
            }

            if ((pid=fork()) == -1)
            {
                perror("fork");
                exit(-1);
            }
            else if ( 0 == pid )
            {
                while(1)
                {
                    P(sid); 
                    printf("child writing ");
                    sleep(1);
                    printf("child finish post ");

                    V(sid);
                }
            }
            else
            {
                while(1)
                {
                    P(sid);
                    printf("parent writing");

                    sleep(1);
                    printf("parent writing finish post ");

                    V(sid);
                }
            }

            return 0;
        }

在该程序中,父子进程都有可能执行P操作成功,因此,两个进程中的提示语句,交替显示。若通过kill命令把其中一个进程杀死,且该进程还没有执行V操作释放资源。若使用SEM_UNDO标志,则操作系统将自动释放该进程持有的信号量,从而使得另外一个进程可以继续工作。若没有这个标志,另外进程将P操作永远阻塞。

因此,一般建议使用SEM_UNDo标志。

原文地址:https://www.cnblogs.com/gary-guo/p/5558861.html