linux的进程间通信——信号量

    xiaoxiao2021-03-25  117

    信号量的本质是一种数据操作锁,它本⾝身不具有数据交换的功能,而是通过控制其他的通信资源(文件,外部设备)来实现进程间通信,它本身只是一种外部资源的标识。信号量在此过程中负责数据操作的互斥、同步等功能。当请求一个使⽤用信号量来表⽰示的资源时,进程需要先读取信号量的值来判断资源是否可 用。大于0,资源可以请求,等于0,无资源可用,进程会进入睡眠状态直⾄至资源可用。 当进程不再使用一个信号量控制的共享资源时,信号量的值+1,对信号量的值进行的增减 操作均为原子操作,这是由于信号量主要的作⽤用是维护资源的互斥或多进程的同步访问。 而在信号量的创建及初始化上,不能保证操作均为原子性。

    为什么要使⽤用信号量? 为了防⽌止出现因多个程序同时访问一个共享资源⽽而引发的一系列问题,我们需要一种⽅方法, 它可以通过⽣生成并使⽤用令牌来授权,在任⼀时刻只能有一个执⾏行线程访问代码的临界区域。 临界区域是指执⾏行数据更新的代码需要独占式地执⾏。而信号量就可以提供这样的一种访 问机制,让一个临界区同一时间只有一个线程在访问它, 也就是说信号量是⽤用来调协进程 对共享资源的访问的。其中共享内存的使⽤用就要⽤用到信号量。

    信号量的工作原理 由于信号量只能进⾏行两种操作等待和发送信号,即P(sv)和V(sv),他们的⾏行为是这样的: (1) P(sv):如果sv的值⼤大于零,就给它减1;如果它的值为零,就挂起该进程的执⾏ (2) V(sv):如果有其他进程因等待sv而被挂起,就让它恢复运⾏,如果没有进程因等待sv⽽挂起,就给它加1.

    Linux的信号量机制 Linux提供了一组精心设计的信号量接口来对信号量进⾏行操作,它们不只是针对二进制信号 量,下面将会对这些函数进⾏行介绍,但请注意,这些函数都是⽤用来对成组的信号量值进行操作的。它们声明在头文件sys/sem.h中。 (1)创建信号量集(semget): 函数原型: int semget(int semid, int senmnum, int cmd, …); 信号量集被建立的情况有两种: (1).如果键的值是IPC_PRIVATE。 (2).或者键的值不是IPC_PRIVATE,并且键所对应的信号量集不存在,同时标志中指定IPC_CREAT。 当调用semget创建一个信号量时,他的相应的semid_ds结构被初始化。ipc_perm中各个量被设置为相应值: sem_nsems被设置为nsems所示的值; sem_otime被设置为0; sem_ctime被设置为当前时间 用法:

    #include <sys/types.h> #include <sys/ipc.h> #include <sys/sem.h> int semget(key_t key, int nsems, int semflg);

    参数: key:所创建或打开信号量集的键值。 nsems:创建的信号量集中的信号量的个数,该参数只在创建信号量集时有效。 semflg:调用函数的操作类型,也可用于设置信号量集的访问权限,两者通过or表示 sem_flags是一组标志,当想要当信号量不存在时创建一个新的信号量,可以和值IPC_CREAT做按位或操作。设置了IPC_CREAT标志后,即使给出的键是一个已有信号量的键,也不会产生错误。而IPC_CREAT || IPC_EXCL则可以创建一个新的,唯一的信号量,如果信号量已存在,返回一个错误。 返回值说明: 如果成功,则返回信号量集的IPC标识符。 如果失败,则返回-1,errno被设定成以下的某个值 EACCES:没有访问该信号量集的权限 EEXIST:信号量集已经存在,无法创建 EINVAL:参数nsems的值小于0或者大于该信号量集的限制;或者是该key关联的信号量集已存在,并且nsems 大于该信号量集的信号量数 ENOENT:信号量集不存在,同时没有使用IPC_CREAT ENOMEM :没有足够的内存创建新的信号量集 ENOSPC:超出系统限制 (2)摧毁和初始化信号量(semctl): 函数原型: int semctl(int semid, int semnum, int cmd, //union semun arg//); 返回值:如果成功,则返回一个正数。 如果失败,则为-1。 EFAULT(arg指向的地址无效) EIDRM(信号量集已经删除) EINVAL(信号量集不存在,或者semid无效) EPERM(EUID没有cmd的权利) ERANGE(信号量值超出范围) 因为信号量一般是作为一个信号量集使用的,而不是一个单独的信号量。所以在信号量集的操作中,不但要知道IPC关键字值,也要知道信号量集中的具体的信号量。这两个系统调用都使用了参数cmd,它用来指出要操作的具体命令。 在系统调用msgctl中,最后一个参数是指向内核中使用的数据结构的指针。我们使用此数据结构来取得有关消息队列的一些信息,以及设置或者改变队列的存取权限和使用者。但在信号量中支持额外的可选的命令,这样就要求有一个更为复杂的数据结构。 系统调用semctl()的第一个参数是信号量集IPC标识符。第二个参数是操作信号在信号集中的编号,第一个信号的编号是0。 参数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设置信号量集中的一个单独的信号量的值。 参数arg代表一个semun的实例。semun是在linux/sem.h中定义的:

    /*arg for semctl systemcalls.*/ union semun{ int val; /*value for SETVAL*/ struct semid_ds *buf; /*buffer for IPC_STAT&IPC_SET*/ ushort *array; /*array for GETALL&SETALL*/ struct seminfo *__buf; /*buffer for IPC_INFO*/ void *__pad; };

    val当执行SETVAL命令时使用。buf在IPC_STAT/IPC_SET命令中使用。代表了内核中使用的信号量的数据结构。array在使用GETALL/SETALL命令时使用的指针。 下面的程序返回信号量的值。当使用GETVAL命令时,调用中的最后一个参数被忽略:

    int get_sem_val(int semid,int semnum) { return (semctl(sid,semnum,GETVAL,0)); }

    下面是一个实际应用的例子:

    #define MAX_PRINTERS 5 printer_usage() { int x; for(x=0; x<MAX_PRINTERS; x++) printf("Printer%d:%d\n\r",x, get_sem_val(sid,x)); }

    下面的程序可以用来初始化一个新的信号量值:

    void init_sem(int semid, int semnum, int initval) { union semun semopts; semopts.val = initval; semctl(sid, semnum, SETVAL, semopts); }

    注意系统调用semctl中的最后一个参数是一个联合类型的副本,而不是一个指向联合类型的指针。 需要注意的是,对于semun联合体,最好自己定义,否则GCC编译器可能会报“semun大小未知”。 (3)p操作,v操作(semop): 用法:

    #include <sys/types.h> #include <sys/ipc.h> #include <sys/sem.h> int semop(int semid, struct sembuf *sops, unsigned nsops);

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

    struct sembuf { unsigned short sem_num; /* semaphore number */ short sem_op; /* semaphore operation */ short sem_flg; /* operation flags */ };

    这三个字段的意义分别为: sem_num:操作信号在信号集中的编号,第一个信号的编号是0。 sem_op:如果其值为正数,该值会加到现有的信号内含值中。通常用于释放所控资源的使用权;如果sem_op的值为负数,而其绝对值又大于信号的现值,操作将会阻塞,直到信号值大于或等于sem_op的绝对值。通常用于获取资源的使用权;如果sem_op的值为0,则操作将暂时阻塞,直到信号的值变为0。 sem_flg:信号操作标志,可能的选择有两种 (1)IPC_NOWAIT //对信号的操作不能满足时,semop()不会阻塞,并立即返回,同时设定错误信息。 (2)IPC_UNDO //程序结束时(不论正常或不正常),保证信号值会被重设为semop()调用前的值。这样做的目的在于避免程序在异常情况下结束时未将锁定的资源解锁,造成该资源永远锁定。

    nsops:信号操作结构的数量,恒大于或等于1。 timeout:当semtimedop()调用致使进程进入睡眠时,睡眠时间不能超过本参数指定的值。如果睡眠超时,semtimedop()将失败返回,并设定错误值为EAGAIN。如果本参数的值为NULL,semtimedop()将永远睡眠等待。

    返回说明: 成功执行时,两个系统调用都返回0。失败返回-1,errno被设为以下的某个值 E2BIG:一次对信号的操作数超出系统的限制 EACCES:调用进程没有权能执行请求的操作,并且不具有CAP_IPC_OWNER权能 EAGAIN:信号操作暂时不能满足,需要重试 EFAULT:sops或timeout指针指向的空间不可访问 EFBIG:sem_num指定的值无效 EIDRM:信号集已被移除 EINTR:系统调用阻塞时,被信号中断 EINVAL:参数无效 ENOMEM:内存不足 ERANGE:信号所允许的值越界 使用信号量的实例: comm.h代码:

    comm.c代码:

    sem.c代码:

    运行结果 例子分析 :因为每个程序都在其进入临界区后和离开临界区前打印一个字符,所以每个字符都应该成对出现,正如你看到的上图的输出那样。在main函数中要输出字符时,每次都要检查信号量是否可用(即stdout有没有正在被其他进程使用)。所以,当一个进程A在调用函数p进入了临界区,输出字符后,调用sleep时,另一个进程B可能想访问stdout,但是信号量的P请求操作失败,只能挂起自己的执行,当进程A调用函数v离开了临界区,进程B马上被恢复执行。然后进程A和进程B就这样一直循环了。

    进程间的资源竞争 看了上面的例子,你可能还不是很明白,不过没关系,下面我就以另一个例子来说明一下,它实现的功能与前面的例子一样,运行方式也一样,都是两个相同的进程,同时向stdout中输出字符,只是没有使用信号量,两个进程在互相竞争stdout。它的代码非常简单,代码如下:

    #include "comm.h" #include <unistd.h> #include <stdlib.h> int main() { pid_t _k = fork(); if(_k == 0) { //child while(1){ printf("A"); fflush(stdout); usleep(12345); printf("A"); fflush(stdout); usleep(12345); } exit(1); DestorySem(semid); } else if(_k > 0) { //father while(1){ printf("B"); fflush(stdout); usleep(12345); printf("B"); usleep(12345); fflush(stdout); } } return 0; }

    运行结果: 由运行结果可以看出,输出结果不是成对存在的,而是随机出现的。

    SEM_UNDO 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时,它将使操作系统跟踪当前进程对这个信号量的修改情况,如果这个进程在没有释放该信号量的情况下终止,操作系统将自动释放该进程持有的信号量。防止其他进程因为得不到信号量而 发生【死锁现象】。 设置sem.sem_flg为 0【终止子进程 出现死锁现象】 (正如上述代码所运行的结果) 设置sem.sem_flg为SEM_UNDO【终止子进程 系统自动V操作 不会出现死锁现象】 结论: 若通过kill命令把其中一个进程杀死,且该进程还没有执行V操作释放资源。若使用SEM_UNDO标志,则操作系统将自动释放该进程持有的信号量,从而使得另外一个进程可以继续工作。若没有这个标志,另外进程将P操作永远阻塞。 因此,一般建议使用SEM_UNDO标志。

    转载请注明原文地址: https://ju.6miu.com/read-25158.html

    最新回复(0)