Linux系统编程——进程间的通信(六)信号量

信号量

信号量(Semaphore),是在多线程环境下使用的一种设施,是可以用来保证两个或多个关键代码段不被并发调用。在进入一个关键代码段之前,线程必须获取一个信号量;一旦该关键代码段完成了,那么该线程必须释放信号量。其它想进入该关键代码段的线程必须等待直到第一个线程释放信号量。为了完成这个过程,需要创建一个信号量。
它是一个计数器。信号量用于实现进程或者线程间的互斥与同步,而不是用于存储进程间通信数据。

特点

(1)信号量用于进程间同步,若要在进程间传递数据需要结合共享内存

(2)信号量基于操作系统的PV 操作,程序对信号量的操作都是原子操作。

(3)每次对信号量的 PV 操作不仅限于对信号量值加 1 或减 1,而且可以加减任意正整数

(4)支持信号量组,也就是有多个信号量。
注:
PV操作是一种实现进程互斥与同步的有效方法。PV操作与信号量的处理相关,P表示通过的意思,V表示释放的意思
原子操作是指不会被线程调度机制打断的操作;这种操作一旦开始,就一直运行到结束,中间不会有任何 context switch(切换到另一个线程)。

创建

最简单的信号量是只能取 0 和 1 的变量,这也是信号量最常见的一种形式,叫做二值信号量(Binary Semaphore)。而可以取多个正整数的信号量被称为通用信号量。

同共享内存一样,系统中同样需要为信号量集定制一系列专有的操作函数(semget,semctl等)。使用函数semget可以创建或者获得一个信号量集ID,函数原型如下:

1 #include <sys/sem.h>
2 // 创建或获取一个信号量组:若成功返回信号量集ID,失败返回-1
3 int semget(key_t key, int nsems, int semflg);
4 // 对信号量组进行操作,改变信号量的值:成功返回0,失败返回-1
5 int semop(int semid, struct sembuf semoparray[], size_t numops);  
6 // 控制信号量的相关信息
7 int semctl(int semid, int sem_num, int cmd, ...);

semget函数

创建或获取一个信号量组:若成功返回信号量集ID,失败返回-1

头文件和函数原型

#include <sys/shm.h>
int semget( key_t key, int nsems, int flag);

key:用来变换成一个标识符,每一个IPC对象与一个key相对应。

nsems该参数只在创建信号量集时有效,是一个大于等于0的值用于指明该信号量集中可用资源数(在创建一个信号量时)。当打开一个已存在的信号量集时该参数值为0。函数执行成功,则返回信号量集的标识符(一个大于等于0的整数),失败,则返回–1。

semflg:调用函数的操作类型,也可用于设置信号量集的访问权限,两者通过 | 表示。

semop函数

对信号量组进行操作,改变信号量的值:成功返回0,失败返回-1

函数原型

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

semid:信号集的识别码,可通过semget的返回值获取。
*sops:指向存储信号操作结构的数组指针,信号操作结构的原型如下

struct sembuf{
    short sem_num; // 除非使用一组信号量,否则它为0
    short sem_op;  // 信号量在一次操作中需要改变的数据,通常是两个数,一个是-1,即P(等待)操作,
                   // 一个是+1,即V(发送信号)操作。
    short sem_flg; // 通常为SEM_UNDO,使操作系统跟踪信号,
                   // 并在进程没有释放该信号量而终止时,操作系统释放信号量
};

sem_flg:信号操作标志,可能的选择有两种

IPC_NOWAIT :对信号的操作不能满足时,semop()不会阻塞,并立即返回,同时设定错误信息。
SEM_UNDO程序结束时(不论正常或不正常),保证信号值会被重设为semop()调用前的值。这样做的目的在于避免程序在异常情况下结束时未将锁定的资源解锁,造成该资源永远锁定。

nsops:信号操作结构的数量,恒大于或等于1。

semctl函数

控制信号量的相关信息

函数原型

int semctl(int semid, int semnum, int cmd, ...);

semid:semget()返回的信号量标识符。

semnum:是操作信号在信号集中的编号,第一个信号的编号是0

cmd:cmd通常是下面的两个值:

SETVAL:

用来把信号量初始化为一个已知的值。p 这个值通过union semun中的val成员设置,其作用是在信号量第一次使用前对它进行设置。

IPC_RMID:
用于删除一个已经无需继续使用的信号量标识符。

cmd中其它命令:

IPC_STAT//读取一个信号量集的数据结构semid_ds,并将其存储在semun中的buf参数中。
IPC_SET//设置信号量集的数据结构semid_ds中的元素ipc_perm,其值取自semun中的buf参数。
IPC_RMID//将信号量集从内存中删除。
GETALL//用于读取信号量集中的所有信号量的值。
GETNCNT//返回正在等待资源的进程数目。
GETPID//返回最后一个执行semop操作的进程的PID。
GETVAL//返回信号量集中的一个单个的信号量的值。
GETZCNT//返回正在等待完全空闲的资源的进程数目。
SETALL//设置信号量集中的所有的信号量的值。
SETVAL//设置信号量集中的一个单独的信号量的值。

如果有第四个参数,它通常是一个union semun结构,定义如下:

union semun {
	int              val;    /* Value for SETVAL */
    struct semid_ds *buf;    /* Buffer for IPC_STAT, IPC_SET */
    unsigned short  *array;  /* Array for GETALL, SETALL */
    struct seminfo  *__buf;  /* Buffer for IPC_INFO
                              (Linux-specific) */
};

在信号量第一次使用前对它进行设置。

示例代码

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <stdio.h>
//int semget(key_t key, int nsems, int semflg);
//int semctl(int semid, int semnum, int cmd, ...);
//int semop(int semid, struct sembuf *sops, unsigned nsops);
union semun {
   int              val;    /* Value for SETVAL */
   struct semid_ds *buf;    /* Buffer for IPC_STAT, IPC_SET */
   unsigned short  *array;  /* Array for GETALL, SETALL */
   struct seminfo  *__buf;  /* Buffer for IPC_INFO
                           (Linux-specific) */
};
void pGetKey(int id)
{
    //struct sembuf sops[2];
    struct sembuf sops;
    sops.sem_num = 0;
    sops.sem_op = -1; //钥匙减一
    sops.sem_flg = SEM_UNDO;
    semop(id,&sops, 1);//1表示只有一个
    printf("pGetKey\n");
}
void vPutKey(int id)
{
    //struct sembuf sops[2];
    struct sembuf sops;
    sops.sem_num = 0;
    sops.sem_op = 1; //钥匙加一
    sops.sem_flg = SEM_UNDO;
    semop(id,&sops, 1);//1表示只有一个
    printf("vPutKey\n");
}
int main()
{
    key_t key;
    key = ftok(".",2);
    int semid;
    semid = semget(key,1,IPC_CREAT | 0666);//1表示信号量集有一个信号量,创建信号量

    union semun initsem;
    //initsem.var = 1;   //表示只有一把钥匙
    initsem.val = 0;   //表示没有钥匙了
    semctl(semid,0,SETVAL,initsem);//信号量初始化,0表示操作第0个信号量,
    					//SETVAL用来设置信号量的值设置为initsem

    int pid = fork();
    if(pid > 0){

        pGetKey(semid);//拿钥匙锁
        printf("this is father\n");
        vPutKey(semid);//放钥匙锁
        semctl(semid,0,IPC_RMID); //删除锁
    }
    else if(pid == 0){

        printf("this is child\n");
        vPutKey(semid); //放钥匙,原先没有钥匙,父进程运行时会阻塞,
        				//只有当子进程将钥匙放进去父进程才会运行
    }
    else{
        printf("fork error\n");
    }
    return 0;
}

在这里插入图片描述
如果没有信号量的话,这个代码运行大部分时间都会先运行父进程,再运行子进程,加入信号量后,父进程先去拿锁,因为原先没有锁,所以父进程会一直阻塞,当运行子进程时,子进程放了一把锁回去,父进程才拿到锁,才可以运行,所以是子进程先运行,父进程后运行。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值