Linux 系统编程 学习:05-进程间通信2:System V IPC(2)
背景
上一讲 进程间通信:System V IPC(1)中,我们介绍了System IPC中有关消息队列、共享内存的概念,以及如何使用。
todo: shm 有关例程
IPC的方式通常有:
- Unix IPC包括:管道(pipe)、命名管道(FIFO)与信号(Signal)
- System V IPC:消息队列、信号量、共享内存
- BSD套接字:Socket(支持不同主机上的两个进程IPC)
我们在这一讲介绍System V IPC的 信号量
导言
生产者-消费者模式是一个十分经典的多线程并发协作的模式,弄懂生产者-消费者问题能够让我们对并发编程的理解加深。
所谓生产者-消费者问题,实际上主要是包含了两类线程,一种是生产者线程用于生产数据,另一种是消费者线程用于消费数据;为了解耦生产者和消费者的关系,通常会采用共享的数据区域,就像是一个仓库,生产者生产数据之后直接放置在共享数据区中,并不需要关心消费者的行为;而消费者只需要从共享数据区中去获取数据,就不再需要关心生产者的行为。但是,这个共享数据区域中应该具备这样的线程间并发协作的功能:
- 如果共享数据区已满的话,阻塞生产者继续生产数据放置入内;
- 如果共享数据区为空的话,阻塞消费者继续消费数据;
- 其实,就相当于一个FLAG,用于通信操作前的检测,以防止数据时序不当。
信号量 (semaphore)
信号量(也叫信号灯)是为了解决同步、互斥问题的较通用的方法。
信号量是一个计数器,常用于处理进程或线程的同步问题,特别是对临界资源的同步访问。
临界资源可以简单的理解为在某一时刻只能由一个进程或线程进行操作的资源,这里的资源
可以是一段代码、一个变量或某种硬件资源。信号量的值大于或等于0时表示可供并发进程使用的
资源实体数;小于0时代表正在等待使用临界资源的进程数。
信号量 的类型:
- Posix 无名信号量、Posix 有名信号量、System V 信号量。
信号量实际是一个整数,它的值由多个进程进行测试(test)和设置(set)。就每个进程所关心的测试和设置操作而言,这两个操作是不可中断的,或称“原子”操作,即一旦开始直到两个操作全部完成。
- 测试和设置操作的结果是:信号量的当前值和设置值相加,其和或者是正或者为负。
- 根据测试和设置操作的结果,一个进程可能必须睡眠,直到有另一个进程改变信号量的值。
一个定义在内核系统中的特殊的变量:
1)该变量最小为0,如果等于0的情况下还去进行P操作,默认情况下就会阻塞
2)P操作就是减操作 proberen ("to test" or "to try")
3)V操作就是加操作 verhogen ("increase")
信号量的有关函数:semget
、semctl
、semop
。
System V 信号灯使用步骤:
1)打开/创建信号灯:申请一个信号量semget得到一个信号量集合,集合中有n个元素
2)信号灯初始化:用 semctl指定集合中第x个元素的初值
3)使用semop进行PV操作(2个进程中,一个P,一个V)
4)删除信号灯:semctl销毁信号量
创建信号量 semget
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semget(key_t key, int nsems, int semflg);
描述:创建一个信号量
参数解析:
key:由ftok函数得到的返回值,如果指定为 IPC_PRIVATE ,则会自动产生新的键值(亲缘进程通信可不使用ftok)
nsems :同时创建的信号量个数(一般只申请3个以内,几乎总是1),成功创建后的信号量都会加入到信号量集中,拥有自己的编号,编号从0开始
semflg : (以下参数可以相 或)
- IPC_CREAT 如果key对应的共享内存不存在,则创建
- IPC_EXIT 如果key对应的共享内存存在,则报错
- mode 共享内存的访问权限( 在有些人的函数中,P(减)操作使用0400,V操作使用0200)
(只要key
相同,那么就对应着一个semid
,一个semid
中可包含nsems
个信号,可以理解成数组)
注意,信号量的创建与下面3个宏有关:(这3个宏在/proc/sys/kernel/sem中可以查到)
- SEMMNS:系统中信号量的最大数目,等于SEMMNI*SEMMSL
- SEMOPM:一次semopt()操作的最大信号量数目
- SEMMNI:系统内核中信号量的最大数目
返回值:
成功返回该信号量的ID;失败:返回 -1 ,同时设置以下errno:
- EACCES :没有访问权限
- EEXIST :在semflg中指定了EEXIST IPC_CREAT和IPC_EXCL,但密钥的信号量集已存在。
- EINVAL nsems小于0或大于每个信号量集(SEMMSL)的信号量限制。
- EINVAL:与密钥对应的信号量集已存在,但NSEM大于该集中的信号量数。
- ENONET:key不存在信号量集,并且semflg未指定IPC_CREAT。
- ENOMEM:系统没有足够的内存申请信号量。
- ENOSPC:超过系统SEMMNI(最大信号量集)、或SEMMNS(系统范围最大信号量)限制
控制信号量 semctl
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semctl(int semid, int semnum, int cmd, ... /*union semun arg*/);
/*使用时,应该先声明以下共用体(每种成员对应一个 #cmd)*/
union semun {
int val; /* SETVAL 用的值 */
struct semid_ds *buf; /* IPC_STAT、IPC_SET用的semid_ds结构 */
unsigned short *array; /* SETALL、GETALL用的数组值 */
struct seminfo *__buf; /* 为控制IPC_INFO提供的缓存 (Linux-specific) */
};
描述:控制信号量中的一个成员(常用于初始化信号量的值)
参数解析:
semid : 信号量的ID
semnum: 信号量中信号的编号值(从0开始)
cmd :
- IPC_STAT 获取属性信息 // 不想讲太详细,这部分用得上的地方少之又少
- IPC_SET 设置属性信息 // 同上
- IPC_RMID 删除信号量,忽略第二个参数
- GEDVAL 返回该信号量元素的值
- SETALL 设置所有信号量元素的值(忽略参数semnum)
- SETVAL 设置该信号量元素的值
arg:(第四个函数应该是union semun arg)
返回值:
失败: -1,设置errno:
- EACCES:参数cmd有GETALL、GETPID、GETVAL、GETNCNT、GETZCNT、IPC_STAT、SEM_STAT、SETALL或SETVAL其中一个值,调用进程对信号量集没有所需的权限,并且没有CAP_IPC_OWNER功能。
- EFAULT:arg.buf或arg.array指向的地址不可访问。
- EIDRM:信号量集已被删除。
- EINVAL:cmd或semid的值无效。或者:对于SEM_STAT操作,在semid中指定的索引值引用了当前未使用的数组槽。
- EPERM:参数cmd设了IPC_SET或IPC_RMID,但调用进程的有效用户ID不是信号量集的创建者(如sem_perm.cuid中所示)或所有者(如sem_perm.uid中所示),并且进程不具有CAP_SYS_ADMIN功能。
- ERANGE:参数cmd的值为SETALL或SETVAL,要设置semval的值(对于集合的某些信号量)小于0或大于实现限制SEMVMX。
成功:
- GETNCNT:semncnt的值。
- GETPID:sempid的值。
- GETVAL:semval的值。
- GETZCNT:semzcnt的值。
- IPC_INFO:内核内部数组中记录所有信号量集信息的最高使用项的索引。(此信息可与重复的SEM-STAT操作一起使用,以获取有关系统上所有信号量集的信息。)
- SEM_INFO:参考 IPC_INFO。
- SEM_STAT:信号量集的标识符,其索引是在semid中给定的。
- 其他命令成功时返回 0
操作信号量 semop
在linux下,pv操作通过调用函数semop
实现。
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semop(int semid, struct sembuf *sops, size_t nsops);
int semtimedop(int semid, struct sembuf *sops, size_t nsops,
const struct timespec *timeout);
/* 用到了以下的结构体 */
struct sembuf{
unsigned short sem_num; /* semaphore number */
short sem_op; /* semaphore operation */
short sem_flg; /* operation flags */
};
描述: 对semid
为semid
的信号量进行批量操作(操作一个或一组信号,P减V加)。
P操作先对信号量元素进行检查,看看其是否大于0,如果不,在设置SEM_UNDO时被系统记录,且不会导致进程阻塞,(未设置时导致进程阻塞,直到信号量元素值大于0解除阻塞)
V操作使得信号量元素指加一,该操作永远不会导致阻塞。
参数解析:
semid : 要进行操作的信号量(由semget函数得到)
sops : 信号量操作结构体
- sem_num:信号量集合中的信号量编号,0代表第1个信号量
- sem_op 操作类型(sem_op的值分为3类):
sem_op > 0:将值添加到semval上,对应与释放某个资源。 (V操作)
sem_op = 0:希望等待到semval值变为0,如果已经是0,则立即返回,否则semzcnt+1,并线程阻塞。 (等0操作)
sem_op < 0:希望等待到semval值变为大于或等于|sem_op|。这对应分配资源。(P操作)如果已经满足条件,则semval减去sem_op的绝对值,否则 semncnt+1并且线程投入睡眠。
- sem_flg 设置信号量的默认操作
IPC_NOWAIT : 设置信号量操作不等待
SEM_UNDO :选项会让内核记录一个与调用进程相关的UNDO记录,如果该进程崩溃,则根据这个进程的UNDO记录自动恢复相应信号量的计数值
nsops :进行操作信号量的个数,即sops结构变量的个数,需大于或等于1。(最常见设置此值等于1,只完成对一个信号量的操作)
返回值:如果所有的操作都执行,则成功返回0。失败返回-1,设置errno:
E2BIG:参数nsops大于SEMOPM,即每个系统调用允许的最大操作数。
EACCES:没有操作的权限,并且没有CAP_IPC_OWNER功能。
EAGAIN:操作无法立即继续,并且在sem flg中指定了IPC NOWAIT,或者在timeout中指定的时间限制已过期。
EFAULT :在sops或timeout参数中指定的地址不可访问。
EFBIG:对于某些操作,sem_num的值小于0或大于或等于集合中的信号量数。
EIDRM :信号量集已被删除。
EINTR:在这个系统调用中阻塞时,线程捕获到一个信号;参考 signal。
EINVAL:信号量集不存在,或者semid小于零,或者nsops是非正值。
ENOMEM:某些操作的sem flg指定了SEM_UNDO,系统没有足够的内存来分配UNDO结构。
ERANGE :对于某些操作,sem_op+semval大于SEMVMX,semval的与实现相关的最大值。
PV 操作的范例
int sem_p(int sem_id, int semnum)
{
// P操作,操作时会检查信号量编号为#semnum的信号量-1,若值在操作前已经为0,阻塞,等到非0为止
static struct sembuf mysembuf;
mysembuf.sem_num = semnum;
mysembuf.sem_op = -1;
mysembuf.sem_flg = 0;
return semop( sem_id, &mysembuf, 1);
}
int sem_v(int sem_id, int semnum)
{
// V操作,操作时让信号量编号为#semnum的信号量值+1
static struct sembuf mysembuf;
mysembuf.sem_num = semnum;
mysembuf.sem_op = 1;
mysembuf.sem_flg = 0;
return semop( sem_id, &mysembuf, 1);
}
信号量的应用实例:
父子进程中使用 信号量。
/*
# Copyright By Schips, All Rights Reserved
# https://gitee.com/schips/
#
# File Name: sem.c
# Created : Thu 19 Mar 2020 04:10:48 PM CST
*/
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <stdlib.h>
int sem_p(int sem_id, int semnum)
{
// P操作,操作时会检查信号量编号为#semnum的信号量-1,若值在操作前已经为0,阻塞,等到非0为止
static struct sembuf mysembuf;
mysembuf.sem_num = semnum;
mysembuf.sem_op = -1;
mysembuf.sem_flg = 0;
return semop( sem_id, &mysembuf, 1);
}
int sem_v(int sem_id, int semnum)
{
// V操作,操作时让信号量编号为#semnum的信号量值+1
static struct sembuf mysembuf;
mysembuf.sem_num = semnum;
mysembuf.sem_op = 1;
mysembuf.sem_flg = 0;
return semop( sem_id, &mysembuf, 1);
}
int main(int argc, char *argv[])
{
pid_t pid;
int semid, ret;
struct sembuf sops;
// ftok("/tmp/", 0x123);
semid = semget(IPC_PRIVATE, 1, IPC_CREAT | 0600);
printf("Sem get id is %d.\n", semid);
ret = semctl(semid, 0, SETVAL, 0);
printf("Init sem : count is 1 with value 0\n");
if(( pid = fork() ) == -1 ) { perror("fork"); }
if(pid == 0) // son
{
printf("Creat son progress for sem_v(+1)\n");
sem_v(semid,0);
printf("sem_v(+1) passed.\n");
}else{ // father
printf("Father progress ofr sem_p(-1)\n");
sleep(5);
sem_p(semid,0);
printf("sem_p(-1) passed.\n");
wait(NULL);
}
semctl(semid, 0, IPC_RMID);
return 0;
}