linux进程通信之信号灯(信号量,semaphore)

信号灯通信,和一般意义的通信不大一样,通信一般是用来收发数据,而信号灯却是用来控制多进程访问共享资源的,利用这一功能,信号量也就可以用做进程同步(实际上是线程间同步)。

信号灯的当前值、有几个进程在等待当前值变为0等等,这些信息,可随时用ipcs -s -i semid命令来查看,调试阶段很有用。

重要数据结构如下:(参考https://blog.csdn.net/xiaoyutao96/article/details/72720718)

<sys/sem.h>
信号灯集的描述
struct semid_ds { 
struct ipc_perm sem_perm; ; //信号灯集的访问权限
struct sem *sem_base; //描述每个信号量的数组的指针
ushort sem_nsems; //信号量集中信号量的数量
time_t sem_otime; //最后一次用semop函数操作该信号灯集的时间
time_t sem_ctime; //本信号灯集的semid_ds被更新时,该值会刷新
… 
};
信号灯的描述:
其中成员sem_base的结构为:
struct sem { 
ushort_t semval; //信号量当前值
short sempid; //最后一次操作该信号量的进程
ushort_t semncnt; //有几个线程在等待semval变为大于当前值 (等待的进程/线程被堵塞)
ushort_t semzcnt; //有几个线程在等待semval变为0,(等待的进程/线程被堵塞)
}; 

信号灯集操作权限的描述:
struct ipc_perm {   // uid、gid、mode的低9bits,这三个域可通过semctl函数的IPC_SET命令来设置
               key_t          __key; /* 本信号灯集是与哪一个key绑定的*/
               uid_t          uid;   /* Effective UID of owner */
               gid_t          gid;   /* Effective GID of owner */
               uid_t          cuid;  /* Effective UID of creator */
               gid_t          cgid;  /* Effective GID of creator */
               unsigned short mode;  /* Permissions */
               unsigned short __seq; /* Sequence number */
           };

资源共享和同步,靠的就是,根据信号量的值,以及等待条件,实现进程/线程的堵塞等待(或者不堵塞而出错返回)


相关API:

1、 int semget(key_t key, int nsems, int semflg);//创建或者打开一个信号灯集(注意:是信号灯集,不是信号灯!)

    形参key、semflg不再赘述,和我的另一篇文章《linux进程通信之消息队列》的队列get函数的形参是一样的,nsems用来设置所创建的信号灯集中信号灯的数目。

2、int semop(int semid, struct sembuf *sops, unsigned nsops);//信号灯操作函数
       int semtimedop(int semid, struct sembuf *sops, unsigned nsops, struct timespec *timeout);//信号灯操作函数

形参semid指定了信号灯集,形参sops指向的数组的每一个元素,每一个元素都指定了针对灯集中的一个灯的操作。
形参:@semid 信号灯集的id
@sops 指向一个struct sembuf类型的数组,数组的大小由形参nsops传入,struct sembuf的成员为:
struct sembuf 
{
    unsigned short sem_num;  /* 要操作的信号灯在信号灯集中的编号,同一个数组中的多个元素可以操作同一个信号量,见下面本函数的用例 */
    short          sem_op;   /* 对信号量的操作 */
    short          sem_flg;  /* operation flags */
}
这一结构描述了一个在特定信号灯上的操作。在调用该函数之前,实际上我们已经静态声明或者动态malloc了一个struct sembuf的数组,数组的大小应该设定为多少?举个例子,我们打算设置第0个信号灯为等待值变为0,然后把第0信号灯的值改为1,还打算把第1个信号灯的值+1,这里我们需要做3个动作,因此,数组就声明为3元素即可。
sembuf结构的成员介绍:(参考https://blog.csdn.net/wanzyao/article/details/55271103)

成员sem_op:进程/线程对某一个信号量的操作,根据操作值的不同,进程/线程会发生不同的响应动作:

· 如果设置的sem_op>0,(进程对该信号量集必须有write权限)该值会被加到信号量的当前值(semval)中,通常用于释放所控资源的使用权(V操作);如果sem_flag指定了SEM_UNDO(还原)标志,那么相应信号量的semadj值会减掉sem_op的值,下面会说明semadj的含义;这种>0的V操作永远不会导致进程/线程堵塞。
· 如果sem_op=0,(进程对该信号量集必须有read权限),等待信号量值变为0。当值不是0时,如果sem_flg设置了IPC_NOWAIT,函数立即返回并设置错误EAGAIN,如果没有设置IPC_NOWAIT,则调用该函数的线程将睡眠,同时semzcnt的值自动+1,直到下列情况时才解除睡眠:①信号量值变成了0(同时semzcnt的值自动-1);②该信号量集被删除,此时该函数立即返回并设置errno;③调用本函数的线程收到了信号,此时该函数立即返回并设置errno,(同时semzcnt的值自动-1),除非建立信号时,设定了SA_RESTART标志;④达到了形参timeout的最大睡眠时间。
· 如果设置的sem_op<0,(进程对该信号量集必须有write权限),而信号量的当前值(semval)≥ abs(sem_op),本函数会立即返回,同时执行semval -= abs(sem_op),这时如果sem_flg还设置了SEM_UNDO标志,那么abs(sem_op)还会被加到变量semadj上;如果信号量的当前值(semval)< abs(sem_op),且设置了IPC_NOWAIT标志,那么本函数立即返回并设置errno,如果没有设置IPC_NOWAIT,变量semncnt自动+1,然后本函数堵塞,直到下面情况时才解除睡眠:①semval变的≥abs(sem_op)时;② 该信号量集被删除,此时该函数立即返回并设置errno;③调用本函数的线程收到了信号,此时该函数立即返回并设置errno,(同时semncnt的值自动-1)除非建立信号时,设定了SA_RESTART标志;④形参timeout睡眠超时。

形参sops指向的数组中的各项操作,会自动按照数组顺序执行,也即:这些操作要么被全部执行完毕,要么一个都不执行。

关于堵塞后被signal信号打断,可参见sigaction函数,简单的说就是:默认情况下,系统调用(如semop)堵塞时,一旦收到了信号,在执行完信号处理函数后,返回系统调用,这时系统调用会立即出错返回;然而,如果信号处理函数收到的第二个实参的成员sa_flag包含了SA_RESTART标志,那么执行完信号处理函数后,返回系统调用,系统调用不会出错返回,而是重新执行一遍系统调用。

关于semadj,对某个进程,在指定SEM_UNDO后,对信号量semval值的修改都会反应到semadj上,当该进程终止的时候,内核会根据semadj的值,重新恢复信号量之前的值,这种机制可以防止进程/线程意外终止时,占用的资源得不到释放。

成员sem_flg:可取的值为IPC_NOWAIT、SEM_UNDO,分别介绍如下:
SEM_UNDO:当进程/线程结束时,该信号灯会自动解除,以避免进程/线程结束后,未解锁的资源永远得不到解锁。
IPC_NOWAIT: 对信号的操作不能满足时,semop()不会阻塞,并立即返回,同时设定错误信息。

形参:@timeout:当semtimedop()调用致使进程进入睡眠时,睡眠时间不能超过本参数指定的值。如果睡眠超时,semtimedop()将失败返回,并设定错误值为EAGAIN。如果本参数的值为NULL,semtimedop()将永远睡眠等待,这时其功能就相当于semop了。

本函数执行成功以后,sempid成员和sem_otime成员会被更新。

返回值:成功返回0;失败返回-1并设置errno

用例(来自官方帮助手册):

功能描述:假设我们已经建立好了一个信号量集,里面包含多个信号量(编号依次为0、1、2···),现在要求我们利用信号量0,来堵塞一个线程,直到信号量0的当前值变为0时,才解除堵塞:

struct sembuf sops[5];//注意:元素的数目和信号量集中信号量的数目没有任何关系,数组的多个元素可以操作同一个信号量
           int semid;
           /* Code to set semid omitted */
           sops[0].sem_num = 0;        /* 要操作的信号量的编号为0 */
           sops[0].sem_op = 0;         /* Wait for value to equal 0 */
           sops[0].sem_flg = 0;        /* 既不IPC_NOWAIT,也不IPC_UNDO */


           sops[1].sem_num = 0;        /* 要操作的信号量的编号为0 */
           sops[1].sem_op = 1;         /* >0 的作用是,把第sem_num个信号量的当前值+sem_op   */
           sops[1].sem_flg = 0;        /* 既不IPC_NOWAIT,也不IPC_UNDO */

           if (semop(semid, sops, 2) == -1) {
               perror("semop");
               exit(EXIT_FAILURE);
           }
代码讲解:前文已经讲过,形参传入的sops所描述的操作,要么一次全执行完,要么一个都不执行:semop第三参数设定要执行sops设定的的前两个操作,第一个sops操作是让线程堵塞地等待第0个信号量的值变为0,然后第2个sops操作把信号量0的当前值+1,这两个操作都执行完以后,线程发现第0个信号量的值不是0,于是就堵塞了。


3、int semctl(int semid, int semnum, int cmd, ...);信号灯集的控制

描述:用于控制信号灯集semid中的第semnum个信号灯。根据cmd的不同,可以有3或4个形参,第四个形参必须为联合体,类型为:

union semun {
               int              val;    /* cmd=SETVAL时使用 */
               struct semid_ds *buf;    /* cmd=IPC_STAT、 IPC_SET时使用 */
               unsigned short  *array;  /* cmd= GETALL、 SETALL时使用 */
               struct seminfo  *__buf;  /* cmd=IPC_INFO (Linux-specific) 时使用*/
           };

cmd可选的值有很多(执行相应命令的进程必须有相应的操作权限、用户组权限等):

(1)IPC_STAT,读信号灯集的状态(也即读本信号灯集的semid_ds数据结构),读出的内容从第4形参传出(联合体中有struct semid_ds),该命令是针对整个信号灯集的,显然该命令会忽略形参semnum。

(2)IPC_SET,设置信号灯集的状态,还是用semid_ds数据结构,该命令并不能设置semid_ds的所有成员,只能设定sem_perm成员的gid、uid、mode三个成员,同时本函数会更新sem_ctime。显然该命令也会忽略形参semnum。

(3)IPC_RMID,删除信号量集(一定会删除成功)。一旦删除,所有被semop堵塞地线程/进程,都会被唤醒,唤醒后的后果是semop函数出错返回,前文介绍过相关内容了了。显然该命令会忽略形参semnum。

(4)IPC_INFO,读取整个操作系统中信号量的限制数目、参数等信息,通过第四形参传出,数据结构为联合体中的struct  seminfo,定义如下。打开宏_GNU_SOURCE之后才能用该命令

struct  seminfo 
{
	int semmap;  /* Number of entries in semaphore
				 map; unused within kernel */
	int semmni;  /* Maximum number of semaphore sets */
	int semmns;  /* Maximum number of semaphores in all
				 semaphore sets */
	int semmnu;  /* System-wide maximum number of undo
				 structures; unused within kernel */
	int semmsl;  /* Maximum number of semaphores in a
				 set */
	int semopm;  /* Maximum number of operations for
				 semop(2) */
	int semume;  /* Maximum number of undo entries per
				 process; unused within kernel */
	int semusz;  /* Size of struct sem_undo */
	int semvmx;  /* Maximum semaphore value */
	int semaem;  /* Max. value that can be recorded for
				 semaphore adjustment (SEM_UNDO) */
};

(5)GETNCNT、GETZCNT、GETPID、GETVAL、SETVAL、SETALL,这几个命令从命名上就能看出它的功能,分别是:

GETNCNT读取有几个线程在等待第semnum个信号量值变大;
GETZCNT读取有几个线程在等待第semnum个信号量值变为0;
GETPID读取最后一个通过semop函数操作第semnum个信号量的那个进程id;
GETVAL读取第semnum个信号量的当前值semval;
SETVAL设置第semnum个信号量的当前值semval;

GETALL读取信号灯集中所有信号灯的当前值semval(通过第四形参联合体中的array成员指向的数组传出),显然该命令也会忽略形参semnum;
SETALL设置信号灯集中所有信号灯的当前值semval(通过第四形参联合体中的array成员指向的数组),显然该命令也会忽略形参semnum;

(6)SEM_INFO、SEM_STAT功能类似于IPC_INFO、IPC_STAT,区别不是很大,自行查阅man手册。

返回值:若失败,则返回-1,并设置errno,若成功,则返回值(一定是≥0的)根据cmd的不同而不同:

例如,读数量返回数量,读pid返回pid,


应用举例,需求描述:子进程和父进程都要打印几个字符串,要求两者的打印不能混在一块,如果父进程先打印了,那么子进程必须等待父进程打印完之后,子进程才能开始打印,如果子进程先打印了,那么情况同理。

//编译: gcc -o sem_exe semaphore.c
#include <stdio.h>
#include <stdlib.h>
 #include <sys/types.h>
#include <sys/ipc.h>
#include <string.h>
#include <errno.h>
#include <sys/sem.h>
#include <unistd.h>
#include <sys/wait.h>
//二值信号量(互斥量)的使用例子
void V(int semid) //占用资源(拿走绿灯)
{
	struct sembuf sops[5];
	sops[0].sem_num = 0;        /* 要操作的信号量的编号为0 */
	sops[0].sem_op = 0;         /* Wait for value to equal 0 */
	sops[0].sem_flg = 0;        /* 既不IPC_NOWAIT,也不IPC_UNDO */

	sops[1].sem_num = 0;        /* 要操作的信号量的编号为0 */
	sops[1].sem_op = 1;         /* >0 的作用是,把第sem_num个信号量的当前值+sem_op   */
	sops[1].sem_flg = SEM_UNDO;        /* IPC_NOWAIT,SEM_UNDO. */

	if (semop(semid, sops, 2) == -1)
	{
		printf("V->semop  failed, infor:%s\r\n", strerror(errno) );
	    exit(1);
	}
}

void P(int semid) //释放资源(归还绿灯)
{
	struct sembuf sops[5];
	sops[0].sem_num = 0;        /* 要操作的信号量的编号为0 */
	sops[0].sem_op = -1;         /* -1 */
	sops[0].sem_flg =  SEM_UNDO;        /* IPC_NOWAIT,SEM_UNDO. */

	if (semop(semid, sops, 1) == -1)
	{
		printf("P->semop  failed, infor:%s\r\n", strerror(errno) );
	    exit(1);
	}
}
int main(int argc, char *argv[])
{
	printf("exe file path: %s\r\n", argv[0] );
	key_t key = ftok(argv[0], 'f');
	if(-1 ==  key)
	{
		printf("ftok failed, infor:%s\r\n", strerror(errno) );
		return -1;
	}
	else
	{
		printf("ftok ok, key = %d\r\n", key );
	}

	int semid = semget(key, 1, IPC_CREAT  | 0666 );//集合中只要一个灯
	if(-1 ==  semid)
	{
		printf("semget failed, infor:%s\r\n", strerror(errno) );
		return -1;
	}
	else
	{
		printf("semget ok, semid = %d\r\n", semid );
	}
	//V(semid);//父进程占用资源
	pid_t pid = fork();
	if(pid == 0 )//子进程
	{
		printf("child process start\r\n");
		V(semid);
		int i;
		for(i = 0; i <= 2; i++)
		{
			printf("child print: %d\r\n",  i);
			sleep(1);
		}
		printf("child released semaphore!\r\n");
		P(semid);
		printf("child process return\r\n");
		_exit(0);
	}
	else if(pid > 0)//父进程
	{
		//sleep(1);
		V(semid);//父进程占用资源
		int i, stat;
		for(i = 0; i <= 2; i++)
		{
			printf("father print: %d\r\n",  i);
			sleep(1);
		}

		printf("father released semaphore!\r\n");
		printf("father is waiting child to end\r\n");
		P(semid);
		if(waitpid(pid, &stat, 0) != pid)
		{
			printf("child process failed!\r\n");
		}
		else
		{
			printf("child process returned value = %d\r\n", stat);
		}
		printf("father process return\r\n");
	}
	else
	{
		printf("fork failed, infor:%s\r\n", strerror(errno) );
		return -1;
	}
	
	return 0;
}


猜你喜欢

转载自blog.csdn.net/qq_31073871/article/details/80891662