System V信号量---多进程同步

1.概述
SystemV信号量并不如Posix信号量那样“好用”,但相比之下它的年代更加久远,但是SystemV使用的却更加广泛(尤其是在老系统中)。在学习Posix信号量的时候,已经大概清楚了二值信号量和计数信号量是什么东西。在接触SystemV信号量之后,这里有一个新的概念叫做:计数信号量集。其实就是把信号量放入数组中,不过都用一些特别的结构封装。

1.信号量结构体
内核为每个信号量集维护一个信号量结构体,可在<sys/sem.h>找到该定义:
struct semid_ds {
    
    
struct ipc_perm sem_perm; /* 信号量集的操作许可权限 */
struct sem *sem_base; /* 某个信号量sem结构数组的指针,当前信号量集
中的每个信号量对应其中一个数组元素 */
ushort sem_nsems; /* sem_base 数组的个数 */
time_t sem_otime; /* 最后一次成功修改信号量数组的时间 */
time_t sem_ctime; /* 成功创建时间 */
};
struct sem {
    
    
ushort semval; /* 信号量的当前值 */
short sempid; /* 最后一次返回该信号量的进程ID 号 */
ushort semncnt; /* 等待semval大于当前值的进程个数 */
ushort semzcnt; /* 等待semval变成0的进程个数 */
};

打开/创建信号量
int semget(key_t key, int nsems, int oflag);
函数功能: 创建一个信号量集或访问一个已经存在的信号量集
返回值: 成功返回非负的标识符,出错返回-1
参数:
key是信号量的键值,多个进程可以通过这个键值访问同一个信号量;

nsems参数指定信号量集合中的信号量数,一般设为1,如果不创建新的信号量集,只是访问一个已经存在的集合,可以把该参数设为0,一旦创建完一个信号量集,就不能改变其中的信号量数;

oflag同open()权限位,IPC_CREAT标示创建新的信号量,如果或上IPC_EXCL,若信号量已存在则出错,如果没有或上IPC_EXCL,若信号量存在也不会出错。

操作/控制信号量
int semctl(int semid, int semnum, int cmd, … /*union semun arg */);

函数功能: 该函数用来直接控制信号量信息.也就是直接删除信号量或者初始化信号量.

返回值: 若成功,根据cmd不同返回不同的值,IPC_STAT,IPC_SETVAL,IPC_RMID返回0,IPC_GETVAL返回信号量当前值;出错返回-1.
在这里插入图片描述

参数:
semid标示操作的信号量集;
semnum标示该信号量集内的某个成员数组下标索引,通常取值0,也就是第一个信号量;
在这里插入图片描述
cmd:指定对单个信号量的各种操IPC_STAT,IPC_GETVAL,IPC_SETVAL,IPC_RMID;
在这里插入图片描述

struct  seminfo {
    
     
     int semmap;  // 信号量映射里的条数,内核未使用 
     int semmni;  // 信号量集合的最大个数 
     int semmns;  // 在所有信号量集合里信号量个数上限  
     int semmnu;  // 系统范围内的 undo 结构最大个数,内核未使用 
     int semmsl;  // 一个信号量集合里信号量个数上限 
     int semopm;  // 执行的最大操作个数  
     int semume;  // 每个进程内 undo 结构最大个数,内核未使用
     int semusz;  // 结构 sem_undo 的尺寸 
     int semvmx;  // 信号量值的上限
     int semaem;  // Max. value that can be recorded for
                    semaphore adjustment (SEM_UNDO) 
};

arg: 可选参数,取决了第三个参数cmd,由此可以看见,有些成员仅仅针对某些命令,这也正是为什么这里用Union而不用Struct,可以节省空间,因为假设当前命令跟某个成员没关的时候,struct依然为这个成员分配空间。

 union semun {
    
    
    int              val;    /* SETVAL使用的值 */
    struct semid_ds *buf;    /* IPC_STAT、IPC_SET 使用缓存区 */
    unsigned short  *array;  /* GETALL,、SETALL 使用的数组 */
    struct seminfo  *__buf;  /* IPC_INFO(Linux特有) 使用缓存区 */
};
注意:该联合体没有定义在任何系统头文件中,因此得用户自己声明,centos6.5/linux/sem.h可以找到

semid_ds 数据结构在头文件 <sys/sem.h> 有如下定义:
struct semid_ds {
    
     
       struct ipc_perm sem_perm;   // 所有者和权限
       time_t          sem_otime;           // 上次执行 semop 的时间  
       time_t          sem_ctime;           // 上次更新时间 
       unsigned short  sem_nsems;   // 在信号量集合里的索引
 };

struct ipc_perm {
    
     
       key_t          __key;    // 提供给 semget()的键 
       uid_t          uid;      // 所有者有效 UID  
       gid_t          gid;      // 所有者有效 GID 
       uid_t          cuid;     // 创建者有效 UID 
       gid_t          cgid;     // 创建者有效 GID
       unsigned short mode;     // 权限 
       unsigned short __seq;    // 序列号
}; 

*int semop(int semid, struct sembuf sops, unsigned nsops);
注释: 用来改变信号量的值,该函数是具有原子性的
功能: 打开一个信号量集合后,对其中一个或多个信号量操作(P/V加减操作)

对于struct sembuf这个结构体来说,其结构定义如下:
struct sembuf{
    
    
    unsigned short sem_num;  /* semaphore number */除非使用一组信号量,否则它为0 
    short          sem_op;   /* semaphore operation */p -1,  v  1
    short          sem_flg;  /* operation flags */0就好 SEM_NOWAIT   SEM_UNDO
};

sem_num指定特定信号量的操作。
sem_op的值分为3类:
a.sem_op > 0:将值添加到semval上,对应与释放某个资源(V操作,生产操作)。
b.sem_op = 0:希望等待到semval值变为0,如果已经是0,则立即返回,否则semzcnt+1,并线程阻塞。
c.sem_op < 0:希望等待到semval值变为大于或等于|sem_op|(希望得到sem_op个数的资源,也就是sem_op的绝对值大于等于剩余的资源数量semval,如果不满足就阻塞,等到条件满足),这对应分配资源(P操作,消费操作)。如果已经满足条件,则semval减去sem_op的绝对值,否则semncnt+1并且线程投入睡眠。
nops是opstr数组中元素数目,通常取值为1。

SystemV信号量的一般编程步骤:

  1. 创建信号量或获得在系统中已存在的信号量
    1). 调用semget().
    2). 不同进程使用同一个信号量键值来获得同个信号量
  2. 初始化信号量
    1).使用semctl()函数的SETVAL操作
    2).当使用二维信号量时,通常将信号量初始化为1
    3.进行信号量PV操作
    1). 调用semop()函数
    2). 实现进程之间的同步和互斥
    4.如果不需要该信号量,从系统中删除
    1).使用semctl()函数的IPC_RMID操作
代码示例:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/sem.h>
#include <sys/ipc.h>

#define DEF_SYSTEMV_SEM 1

union semun {
    
    
    int val;
    struct semid_ds *buf;
    unsigned short *array;
};


int init_sem(int sem_id,int init_value) {
    
    
    union semun sem_union;
    sem_union.val=init_value;
    if (semctl(sem_id,0,SETVAL,sem_union)==-1) {
    
    
        perror("Error Sem init");
        exit(1);
    }
    return 0;
}

int destory_sem(int sem_id){
    
    
    union semun sem_union;
    if (semctl(sem_id,0,IPC_RMID,sem_union)==-1){
    
    
        perror("Error Sem delete");
        exit(1);
    }
    return 0;
}

int sem_wait(int sem_id) {
    
    
    struct sembuf sem_buf;
    sem_buf.sem_num=0;//信号量编号
    sem_buf.sem_op=-1;//P操作
    sem_buf.sem_flg=SEM_UNDO;//系统退出前未释放信号量,系统自动释放
    if (semop(sem_id,&sem_buf,1)==-1) {
    
    
        perror("Error sem_wait");
        exit(1);
    }
    return 0;
}
int sem_signal(int sem_id) {
    
    
    struct sembuf sem_buf;
    sem_buf.sem_num=0;
    sem_buf.sem_op=1;//V操作
    sem_buf.sem_flg=SEM_UNDO;
    if (semop(sem_id,&sem_buf,1)==-1) {
    
    
        perror("Error sem_signal");
        exit(1);
    }
    return 0;
}

int main() {
    
    
    pid_t pid;

#if DEF_SYSTEMV_SEM
    key_t sem_key=ftok(".",'AAAA');
	int sem_id=semget(sem_key,1,0666|IPC_CREAT);

    init_sem(sem_id,1);
#endif

    if ((pid=fork())<0) {
    
    
        perror("fork error!\n");
        exit(1);
    } else if (pid==0) {
    
    

#if DEF_SYSTEMV_SEM
        sem_wait(sem_id);
#endif

        printf("Child befor sleep...\n");
        sleep(20);
        printf("Child after sleep...\n");

#if DEF_SYSTEMV_SEM
        sem_signal(sem_id);
#endif

        exit(0);
    } else {
    
    

#if DEF_SYSTEMV_SEM
        sem_wait(sem_id);
#endif

        printf("Parent befor sleep...\n");
        sleep(20);
        printf("Parent after sleep...\n");

#if DEF_SYSTEMV_SEM
        sem_signal(sem_id);
        waitpid(pid,0,0);
        destory_sem(sem_id);
#endif

        exit(0);
    }

}

运行结果:
$ ./a.out
Parent befor sleep…
Parent after sleep…
Child befor sleep…
Child after sleep…
可以看到是父进程执行完后才执行子进程。

如果把条件编译宏设为0 “#define DEF_SYSTEMV_SEM 0”
取消System V 信号量,运行结果为
$ ./a.out
Parent befor sleep…
Child befor sleep…
Parent after sleep…
Child after sleep…
可以看到父子进程之间临界区代码执行已经乱序

通过多个信号量解决经典的生产者—消费者的问题:
单个信号量单单依靠一个信号量是无法实现的,生产者-消费者模型中,两个进程共用临界资源,所以它们首先互斥;此外,如果缓冲区满,生产者是不能生产的,所以生产者进程受到消费者进程的制约;如果缓冲区空,消费者是不能消费的,所以消费者进程受到生产者进程的制约。但你用一个信号量去控制同步的话,因为V(n)没有条件,所以生产者进程实际上不受制约,永不阻塞,只有消费者进程受到生产者的制约,所以,必须要用到多个信号量,才能解决问题。

下一篇博客 systemV信号量实现进程间共享内存同步。

猜你喜欢

转载自blog.csdn.net/wangrenhaioylj/article/details/109131736