Linux进程间通信-信号量 原创 Linux平台 2022年2月18日 21:18 夏至未至 1614 当前内容 6426 字,在路上,马上到,马上到 ### 目录 [TOC] ### 信号量介绍 #### 信号量本质 信号量的本质是一种`数据操作锁`,它本身不具有数据交换的功能,而是通过控制其他的通信资源(文件,外部设备)来实现进程间通信,它本身只是一种外部资源的标识。`信号量在此过程中负责数据操作的互斥、同步等功能` #### 信号量作用 当请求一个使用信号量来表示的资源时,进程需要先读取信号量的值来判断资源是否可用。大于0,资源可以请求,等于0,无资源可用,进程会进入睡眠状态(进程挂起等待)直至资源可用。 当进程不再使用一个信号量控制的共享资源时,信号量的值+1(信号量的值大于0),对信号量的值进行的增减操作均为原子操作,这是由于信号量主要的作用是维护资源的互斥或多进程的同步访问。 而在信号量的创建及初始化上,不能保证操作均为原子性。 ### 为何使用信号量 为了防止出现因多个程序同时访问一个共享资源而引发的一系列问题,我们需要一种方法, 它可以通过生成并使用令牌来授权,在任一时刻只能有一个执行线程访问代码的临界区域。 临界区域是指执行数据更新的代码需要独占式地执行(这个房子暂时我一个人可以用, 房子好比临界区域)。而信号量就可以提供这样的一种访问机制,让一个临界区同一时间只有一个线程在访问它, 也就是说信号量是用来调协进程对共享资源的访问的。其中共享内存的使用就要用到信号量。 ### 信号量原理 由于信号量只能进行两种操作等待和发送信号,即P(sv)和V(sv),他们的行为是这样的: - P(sv):如果sv的值大于零,就给它减1;如果它的值为零,就挂起该进程的执行 - V(sv):如果有其他进程因等待sv而被挂起,就让它恢复运行,如果没有进程因等待sv而挂 起,就给它加1. 举个例子,就是两个进程共享信号量sv,一旦其中一个进程执行了P(sv)操作,它将得到信号 量,并可以进入临界区,使sv减1。而第二个进程将被阻止进入临界区,因为当它试图执行 P(sv)时,sv为0,它会被挂起以等待第一个进程离开临界区域并执行V(sv)释放信号量,这时 第二个进程就可以恢复执行。 ### 信号量实现 Linux提供了一组精心设计的信号量接口来对信号量进行操作,它们不只是针对二进制信号 量,下面将会对这些函数进行介绍,但请注意,这些函数都是用来对成组的信号量值进行操作的。它们声明在头⽂文件sys/sem.h中 #### semget 函数 它的作用是创建一个新信号量或取得一个已有信号量,原型为: int semget(key_t key, int num_sems, int sem_flags); ##### 入参 1. 第一个参数key是整数值(唯一非零),不相关的进程可以通过它访问一个信号量,它代表程序可能要使用的某个资源,程序对所有信号量的访问都是间接的,程序先通过调用semget函数并提供一个键,再由系统生成一个相应的信号标识符(semget函数的返回值),只有semget函数才直接使用信号量键,所有其他的信号量函数使用由semget函数返回的信号量标识符。如果多个程序使用相同的key值,key将负责协调工作。 2. 第二个参数num_sems指定需要的信号量数目,它的值几乎总是1。 3. 第三个参数sem_flags是一组标志,当想要当信号量不存在时创建一个新的信号量,可以和值IPC_CREAT做按位或操作。设置了IPC_CREAT标志后,即使给出的键是一个已有信号量的键,也不会产生错误。而IPC_CREAT | IPC_EXCL则可以创建一个新的,唯一的信号量,如果信号量已存在,返回一个错误。 ##### 返回值 semget函数成功返回一个相应信号标识符(非零),失败返回-1. #### semop 函数 它的作用是改变信号量的值,原型为: int semop(int sem_id, struct sembuf *sem_opa, size_t num_sem_ops); ##### 入参 1. 参数 sem_id 为信号量集的标识符 2. 参数 sembuf 指向进行操作的结构体数组的首地址,结构的定义如下: struct sembuf { short sem_num;//除非使用一组信号量,否则它为0 short sem_op;//信号量在一次操作中需要改变的数据,通常是两个数,一个是-1,即P(等待)操作, //一个是+1,即V(发送信号)操作。 short sem_flg;//通常为SEM_UNDO,使操作系统跟踪信号, //并在进程没有释放该信号量而终止时,操作系统释放信号量 }; 3. 参数 nsops 指出将要进行操作的信号的个数。 ##### 返回值 semop 函数调用成功返回 0,失败返回 -1。 #### semctl 函数 该函数用来直接控制信号量信息,它的原型为: int semctl(int sem_id, int sem_num, int command, ...); ##### 入参 1. sem_id:信号集的标识符,即是信号表的索引。 2. sem_num:信号集的索引,用来存取信号集内的某个信号。 3. command:需要执行的命令,有效值有 IPC_STAT //将与semid关联的内核数据结构拷贝到由arg.buf指针指向的内存区。 IPC_SET //将由arg.buf指针指向的semid_ds的一些成员写入相关联的内核数据结构,同时更新它的sem_ctime成员。 IPC_RMID //立即删除信号集,唤醒所有被阻塞的进程。 IPC_INFO //Linux特有命令,返回系统范围内关于信号集的制约和其它参数 SETVAL // 用来把信号量初始化为一个已知的值。p 这个值通过union semun中的val成员设置,其作用是在信号量第一次使用前对它进行设置。 如果有第四个参数,它通常是一个union semum结构,定义如下: union semun { int val; struct semid_ds *buf; unsigned short *arry; }; ##### 返回值 成功执行时,根据不同的命令返回不同的非负值 GETNCNT //返回semncnt的值 GETPID //返回sempid的值 GETVAL //返回semval的值 GETZCNT //返回semzcnt的值 IPC_INFO //返回内核内部关于信号集信息的最大可用入口索引 SEM_INFO //如同IPC_INFO. SEM_STAT //返回信号集标识 剩下的命令返回0。 失败返回-1,errno被设为以下的某个值 EACCES:访问出错,权能不允许 EFAULT:arg.buf 或 arg.array所指的空间不可访问 EIDRM:信号集已被删除 EINVAL;参数无效 EPERM:权能不允许 ERANGE:给出的参数无效 ### 上机实操 #### 代码实现 #include #include #include #include #include #include #include #include union semun { int val; struct semidds *buf; unsigned short *arry; }; static int semid = 0; static int set_semvalue(); static void del_semvalue(); static int semaphore_p(); static int semaphore_v(); int main(int argc, char *argv[]) { printf("https://www.codecomeon.com/ \n"); char message = 'X'; int i = 0; // 创建信号量 semid = semget((keyt)1234, 1, 0666 | IPC_CREAT); if(argc > 1) { // 程序第一次被调用,初始化信号量 if(!set_semvalue()) { fprintf(stderr, "Failed to initialize semaphored") exit(EXITFAILURE); } message = argv[l][0]; sleep(2); } for(i = 0; i < 10; ++i) { // 进入临界区 if (!semaphore_p()) exit(EXIT_FWₚE); // 向屏幕中输出数据 printf("%c", message); // 清理缓存区,然后休眠随机时间 fflush(stdout); sleep(rand() % 3); // 离开临界区前,再一次向屏幕输出数据 printf("%c", message); fflush(stdout); // 离开临界区,休眠随机时间后继续循环 if(!semaphore_v()) exit(EXITFAILURE); sleep(rand() % 2); } sleep(10); printf("\n%d - finished\n", getpidO); if(argc > 1) { // 如果程序是第一次被调用,则在退出前输出信号 sleep(3); del_semvalue(); } exit(EXITSUCCESS); } static int set_semvalue(){ // 用户初始化信号量,在使用信号量前必须这样做 printf("https://www.codecomeon.com/ \n"); union semun sem_union; sem_union.val = 1; if(semctl(sem_id, 0, SETVAL, sem_union) == -1) return 0; . . , return 1; } static void del_semvalue(){ // 删除信号 union semun sem_union; if(semctl(sem_id, 0, IPC_RMID, sem_union) == -1) fprintf(stderr, "Failed to delete semaphored\n"); } static int semaphore_p() { // 对信号量做减1操作,即等待P(sv) struct sembuf sem_b; sem_b.sem_num = 0; sem_b.sem_op = -1; //P() sem_b.sem_flg = SEM_UNDO; if(semop(sem_id, &sem_b, 1) == -1) { fprintf(stderr, "semaphorep failedXn"); return 0; } return 1; } static int semaphore_v() { // 这是一个释放操作,它使信号量变的可用,即发送信号 V(sv) struct sembuf sem_b; sem_b.sem_num = 0; sem_b.sem_op = 1;//V() sem_b.sem_flg = SEM_UNDO; if(semop(sem_id, &sem_b, 1) == -1) { fprintf(stderr, "semaphorev failedXn"); return 0; } return 1; } #### 代码分析 这个程序的临界区为main函数for循环的semaphore_p和semaphore_v函数中间的代码。分析看:同时运行一个程序的两个实例,注意第一次运行时,要加上一个字符作为参数,例如本例中的字符‘O’,它用于区分是否为第一次调用,同时这个字符输出到屏幕中。因为每个程序都在其进入临界区后和离开临界区前打印一个字符,所以每个字符都应该成对出现,正如你看到的上图的输出那样。在main函数中循环中我们可以看到,每次进程要访问stdout(标准输出),即要输出字符时,每次都要检查信号量是否可用(即stdout有没有正在被其他进程使用)。所以,当一个进程A在调用函数semaphore_p进入了临界区,输出字符后,调用sleep时,另一个进程B可能想访问stdout,但是信号量的P请求操作失败,只能挂起自己的执行,当进程A调用函数semaphore_v离开了临界区,进程B马上被恢复执行。然后进程A和进程B就这样一直循环了10次。 本文标题: Linux进程间通信-信号量 本文作者: 夏至未至 发布时间: 2022年2月18日 21:18 最近更新: 2022年2月19日 08:30 原文链接: 许可协议: 署名-非商业性-禁止演绎 4.0 国际(CC BY-NC-ND 4.0) 请按协议转载并保留原文链接及作者 进程间通信(8) 信号量(1) 上一个 Linux下进程间通信-套接字 下一个 Linux下进程间通信-共享内存 当前文章评论暂未开放,请移步至留言处留言。