[Linux]——IPC进程通信之信号量

System V 信号量

我们在进程间通信专题已经详细的介绍过了管道和共享内存的进程间通信机制。但是其实还有一种非常独特的进程间通信机制,也就是本篇博客将为大家介绍的System V机制的信号量。

为什么说他独特,因为之前我们介绍的通信机制最终都会有一个数据交互的场所,而信号量其实是一个计数器。有的同学会提出质疑,计数器怎么能作为进程通信的机制呢?别忘了,进程控制也是进程通信的目的。接下来,笔者就为大家详细介绍今天的主角。

如何理解信号量?

上面说了,信号量其实是一个非负值计数器,这个计数器表示现在系统中有多少资源能够被申请使用。当进程申请信号量时这个计数器就会减少,反之归还资源时这个计数器就会加加。每当信号量为0时表示没有资源可以被申请,此时再来申请资源的进程就会被暂时挂起,直到信号量不为零时才唤醒。

由此可见信号量一定维持了一个属于自己的等待队列。其实信号量很好理解,我们拿生活中一个形象的栗子说明

  • 大家一定在景点上过可移动的卫生间(栗子赶紧怪怪的,但是很形象)。卫生间当有人进入时,卫生间的总数量就会减一,当有人出来时,卫生间的数量就会被加一。如果此时不凑巧所有的卫生间中都有人,那么你就要在门外等待,直到有人出来时你才能使用。

类比进程,每个卫生间被占用就好比向信号量申请资源,相反归还资源。厕所满时就要像进程一样等待。而在System V中信号量申请和归还对应了下面的俩个操作。

  • P(sv): 如果sv的值大于零,就给它减一; 如果它的值为零,否则就挂起该进程(对应申请资源)

  • V(sv): 如果有其他进程因等待sv而被挂起,就让他恢复运行,如果没有进程因等待sv而挂起,就给它+1.(对应归还资源)

信号量管理

对于操作系统来说,他需要管理信号量。一般需要对信号量完成下里的几种操作:

  • 测试控制某个资源的信号量
  • 若信号量为正,被某个进程申请,那么就要对信号量进行P操作,进程会将信号量值减去1,表示它使用了一个资源单位
  • 如果信号量为0,那么就要将当前进程暂时挂起,等信号量不为零时将进程唤醒

为了正确地实现信号量,信号量值的测试及减1操作应当是原子操作.。为此,信号量通常是在内核中实现的,这个不难理解。常用的信号量形式被称为二元信号量.,它控制单个资源,其初始值为1.。但是,一般而言,信号量的初值可以是任意一个正值,该值表明有多少个共享资源单位可供共享应用。

现在相信你对信号量应该已经有了初步的理解,但是其实真正System V信号量并没有像我们想的那样单纯是个计数器,而是每次申请时,它可以帮我们申请一组信号量。

[Linux]——IPC进程通信之信号量_第1张图片
来看看操作系统是如何描述这个信号量集的:

struct semid_ds
{
	struct ipc_perm sem_perm;//信号量的操作权限信息
	unsigned short  sem_nsems;//信号集中有多少个信号量
	time_t          sem_otime;//最后一次调用semop的时间
	time_t          sem_ctime;//最后一次调用semctl的时间
};

而信号量集中一个一个信号量通常使用一个匿名结构体来描述:

struct ...
{
	unsigned short semval;//信号量值
	pid_t          sempid;//最后一次执行semop操作的进程数量
	unsigned 	   semncnt;//等待信号量增加的进程数
	unsigned       semzcnt;//等待信号量变为零的进程数
}

上面结构体中提到了几个还没有说的函数不用关心,下面讲解后同学们可以倒回来看看。现在我们就结合函数,一起来看看如何使用信号量。

信号量系统调用

semget函数:用来创建一个新信号量集,或者用来获取已经有的信号量集

int semget(key_t key, int num_sems, int sem_flags); 

参数介绍:

  • key:使用ftok函数生成,不懂的同学man一下这个函数,很简单。这个key就是不同进程访问同一个信号量的最有力凭证。用来标识系统中一个唯一的信号量集。
  • num_sems:信号量集中信号量的个数,大多数情况下都是1。因为设置为1就可以把整个集合看作为一个计数器
  • sem_flags:和共享内存选项相同。提供IPC_CREAT和IPC_EXCL等选项,他们直接使用"|"连接,也可以|上信号量的权限(八进制)

函数调用成功后就会在系统中为我们创建一个semid_ds信号集合,并且初始化其中的某些部分。函数返回值返回一个此信号集的标识符,失败时返回-1。

semop函数:用来改变信号量的值

int semop(int sem_id, struct sembuf *sem_opa,size_t num_sem_ops);

参数介绍:

  • sem_id:填充semget函数的返回值
  • num_sem_ops:先说第三个参数,第二个参数是一个结构体数组,此参数代表第二个参数数组的大小

单独说第二个参数说明他肯定不一般,说实话信号量这个接口设计的真的是非常不友好,恶心程度快赶上了select了emmmm。。。。

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

sem_flg:顺序原因我们先介绍这个标识,这个标识一般为SEM_UNDO和SEM_NOWAIT

  • SEM_UNDO:让进程对一个信号量p操作之后呢,无论是异常退出还是正常退出都会自动的释放该信号量资源
  • SEM_NOWAIT:填充后无论是否对信号量操作成功,都直接返回。这其实类似于非阻塞的I/O

sem_num:要操作信号量的编号,拿我们最开始那个田字格图来说,操作信号量1就填1,不过之前我们也说了,一般我们信号量集合中只有一个信号量,所以大多数情况下都是0

sem_op:此变量是一个整数,会分为三种情况,然而再谈三种情况前,我们又要提到信号量调整数的概念

  • 在Linux下,每个进程都有个信号量调整数,它是int型的一个数字。它通常用来记录对信号量操作中负数的统计。通常我们进行释放资源时,sem_op为正值时,调整值就减去sem_op的绝对值,进行申请资源时sem_op为负值,调整值就加上sem_op的绝对值。所以可以看出这个调整数只是统计了该进程申请的信号量数。这个数在内核中为semadj。其实不难理解为什么设置了SEM_UNDO能够保证无论是异常退出还是正常退出都会自动的释放该信号量资源,每当进程退出这个值不为零时系统就会自动的归还信号量资源。

现在来谈sem_op设置为不同数时的三种情况:

sem_op为正数:最易于处理的情况是sem_op为正值,这对英语进程释放占用的资源数。sem_op值会加到信号量的值上,如果指定了undo标志,则也要从该进程的此信号量调整值semadj中减去sem_op的值

若se_op为负数:则表示要获取由该信号量控制的资源,如果该信号量的值大于或等于sem_op的绝对值,则从信号量值中减去sem_op的绝对值。 这能保证信号量的结果值大于等于0,如果指定了undo标志,则sem_op的绝对值也加到该进程的此信号量调整值上。但是如果信号量的值小于sem_op的绝对值,情况就不一样了。

  • 若指定了IPC_NOWAIT,则semop出错返回EAGAIN。若未指定IPC_NOWAIT,则该信号量的semncnt+1(因为调用进程将进入休眠模式),然后调用进程被挂起直至下列事情之一发生

  • i.此信号量值变成大于等于sem_op的绝对值(既某个进程已经释放了某些资源). 此信号量的semncnt的值减一,并且从信号量值中减去sem_op的绝对值.如果指定了undo标志,则sem_op的绝对值也加到该进程的此信号量调整值当中

  • ii.从系统中删除了此信号量,自这种情况下,函数出错返回EIDRM

  • iii. 进程捕捉到一个信号,并从信号处理程序返回,在这种情况下,此信号量的semncnt的值减去1,并且函数出错返回EINTR

若sem_op为0:这表示调用进程希望等待到该信号量的值变为0

  • 若指定了IPC_NOWAIT,则semop出错返回EAGAIN。若未指定IPC_NOWAIT,则该信号量的semnznt+1(因为调用进程将进入休眠模式),然后调用进程被挂起直至下列事情之一发生

  • i.此信号量的值变成0,此信号量的semzcnt减去一(因为调用进程已结束等待)

  • ii.从系统中删除此信号量,在这种情况下,函数出错返回EIDRM

  • iii.进程捕捉到一个信号,并且从信号处理程序返回. 在这种情况下,此信号量的semzcnt值减去1,并且函数出错返回EIINTR

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

看到这里的老铁没有吐吧,反正笔者先吐为敬。别急,还有更恶心的东西,先来看semctl函数原型:

int semctl(int sem_id, int sem_num, int command, ...); 

参数介绍:

  • sem_id:填充semget函数的返回值

  • sem_num: 操作信号在信号集中的编号,同sembuf中的sem_num

  • command :命令,标识要进行的操作

  • 参数4:一个不简单的省略号

命令 含义
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 设置信号量集中的一个单独的信号量的值

第四个参数在使用有些选项时需要用到,比如SETVAL。仔细的读者发现我们即使讲到现在还是没有将信号量的值初始化,所以我们使用SETVAL时就需要使用第四个参数

union semun
{  
    int val; //用于SETVAL
    struct semid_ds *buf;  //用于IPC_SET和IPC_STAT
    unsigned short *arry;  //SETALL和GETALL使用
};

简单的信号量使用实例

此时就实现了进程间的同步,信号量接口真的设计的非常不友好

#include
#include
#include
#include
#include


union semun{
	int val;
	struct semid_ds *buf;
	unsigned short *arry;
	struct seminfo* __buf;
};

void pv(int sem_id, int op)
{
	struct sembuf sem_b;
	sem_b.sem_num = 0;
	sem_b.sem_op = op;
	sem_b.sem_flg = SEM_UNDO;
	semop(sem_id, &sem_b, 1);
}

int main()
{
	int sem_id = semget(IPC_PRIVATE, 1, IPC_CREAT | IPC_EXCL | 0644);
	union semun sem_un;
	sem_un.val = 1;//初始化信号量为1
	semctl(sem_id, 0, SETVAL, sem_un);

	pid_t id = fork();
	if (id < 0)
	{
		return 1;
	}
	else if (id == 0)
	{
		printf("child try to get binary sem\n");
		pv(sem_id, -1);
		printf("chlid get the sem and release it after 5 seconds\n");
		sleep(5);
		pv(sem_id, 1);
		exit(0);
	}
	else
	{
		printf("parent try to get binary sem\n");
		pv(sem_id, -1);
		printf("parent get the sem and release it after 5 seconds\n");
		sleep(5);
		pv(sem_id, 1);
	}
	waitpid(id, NULL, 0);
	semctl(sem_id, 0, IPC_RMID, sem_un);
	return 0;
}

总结

看了信号量的系统调用接口,我感觉我上辈子就是个蜀道啊。不过多看几遍,应该也不是太难理解。

你可能感兴趣的:(Linux)