公告

Gentoo交流群:838664909 欢迎您的加入

#1 2024-03-10 18:29:41

batsom
管理团队
注册时间: 2022-08-03
帖子: 607
个人网站

Gentoo 之 System V信号量

1.信号量概述

信号量也叫信号灯,用于进程/线程同步或互斥的机制
信号灯的类型 : Posix无名信号量 主要用于主要用于线程间的同步和互斥
Posix有名信号量 主要用于进程间同步和互斥
System V 信号量

信号量与其他进程间通信机制不大相同,它主要提供对进程共享资源访问控制机制,相当于内存中的标志,进程可以根据它判定是否能访问某些共享资源,同时,进程也可以修改该标志。除了同于访问控制外,可以用于进程同步。信号量本质上是一个非负的整数计数器。


System V信号量不是用来在进程间传输数据的,而是用来同步进程的动作。

信号量的一个常见用途是同步对一块共享内存的访问以防止出现一个进程在访问共享内存的同时另一个进程更新这块内存的情况,

在控制进程的动作方面,信号量本身并没有任何意义,它的意义仅由使用信号量的进程赋予其的关联关系来确定。

进程之间会达成协议将一个信号量与一种共享资源关联起来,如一块共享内存区域。信号量还有其他用途,如在 fork()之后同步父进程和子进程。


使用Svstem V信号量的常规操作步骤:

    使用semget()创建或者打开一个信号量集。

    使用semctl() SETVAL 或 SETALL 操作初始化集合中的信号量(只有一个进程需要完成这个任务)。

    使用semop()操作信号量的值。使用信号量的进程通常会使用这些操作来标识一种共享资源的获取和释放。

    当所有进程都不再需要使用信号量集之后semctl() IPC_RMID操作删除这个集合(只有一个进程需要完成这个任务)。

    System V信号量是以分配被称为信号量集的组为单位进行的。在使用semget()系统调用创建集合的时候需要指定集合中的信号量数量。

    虽然同一时刻通常只会操作一个信号量,但通过semop()系统调用可以原子的在同一个集合中的多个信号量之上执行一组操作。

二、使用Svstem V信号量
1.信号量数据结构

信号量数据结构是信号量程序设计中经常使用的数据结构,由于在之后的函数经常用到,这里将结构的原型列出来,便于查找:

//每个信号量集,内核维护如下信息结构:<sys/sem.h>
struct semid_ds
{
  struct ipc_perm 	sem_perm;		/* 操作权限结构 */
  struct sem		*sem_base;		//指向sem结构数组指针
  time_t 			sem_otime;			/* 上次semop()时间 */
  time_t 			sem_ctime;			/* 上次由semctl()更改的时间 */
  ushort 	 		sem_nsems;		/* 集合中的信号量数量 */
};

//ipc_perm含有当前信号量访问权限
struct ipc_perm{
	uid_t uid;
	gid_t gid;
	uid_t cuid;
	gid_t cgid;
	mode_t mode;
	ulong_t seq;
	key_t key;
}

//维护某个信号量的一组的内部数据结构,一个信号量集的每个成员如下这个结构描述:
struct sem{
ushort_t 	semval;
short 		sempid;
ushort_t 	semncnt;
ushort_t 	semzcnt;
}
 

除维护一 个信号量集内每个信号量的实际值之外,内核还给该集合中每个信号量维护另外三个信息:对其值执行最后 一 次操作的进程的进程ID 、等待其值增长的进程数计数以及等待其值变为0的进程数计数。

2.新建信号量semget()

semget()函数用于创建一个新的信号量集合,或者访问现有的集合。

其原型如下,其中第1个参数key是ftok生成键值,第2个参数nsems参数可以指定新的集合中应该创建的信号量的数目,第3个参数semflsg是打开信号的方式。

#include<sys/types.h>
#include<sys/sem.h>
int semget(key_t key,int nsems,int semflg);
//key是ftok生成键值
//nsems参数可以指定新的集合中应该创建的信号量的数目
//semflsg是打开信号的方式

semflsg属性值:

    IPC_CREAT: 如果内核中不存在这样的信号量集合,则把它创建出来。
    IPC_EXCL:
        当与IPC_CREAT一起使用时,如果信号量集合早已存在,则操作将失败。
        单独使用IPC _ CREAT, semget()或者返回新创建的信号量集合的信号量集合标识符;或者返回早已存在的具有同 个关键字值的集合的标识符。
        同时使用IPC_EXCL和IPC_CREAT, 那么将有两种可能的结果:如果集合不存在,则创建一个新的集合;如果集合早已存在,则调用失败,并返回-1。
        IPC_EXCL本身是没有什么用处的,但当与IPC_CREAT组合使用时,它可以用于防止为了访问而打开现有的信号量集合。

//CreateSem()函数按照用户的键值生成一个信号量,把信号量的初始值设为用户输入的value。
typedef int sem_t;

union semun{		//信号量操作的联合结构
		int val;	//整型变量
		struct semid_ds *buf;//semid_ds结构指针
		unsigned	short  *array;//数组类型
		}arg;//全局变量
sem_t CreateSem(key_t key,int value)//建立信号量,魔数key和信号量的初始值value
{
	union	semun	sem; //信号量结构变量
	sem_t	semid;//信号量ID
	sem.val = val;//设置初值
	semid = semget(key,0,IPC_CREAT|0666);//获得信号量ID
	if(-1 == semid)
	{
		printf("create semaphore error\n");
		return -1;
	}
	semctl(semid,0,SETVAL,sem);//发送命令,建立value个初始值的信号量
	return semid;
}

3.控制信号量参数semctl()函数

与文件操作的ioctl()函数类似,信号量的其他操作是通过函数semctl()来完成。函数semctl()的原型如下:

#include<sys/types.h>
#include<sys/ipc.h>
#inlcude<sys/sem.h>

int semctl(int semid,int semnum,int cmd,/*union semun arg*/...);

//第4个参数指向如下联合体:
union semun{
	int val;//当执行SETVAL命令时将用到这个成员,它用于指定要把信号量设置成什么值。
	struct semid_ds *buf;//在命令IPC_STAT/IPC_SET中使用。它代表内核中所使用的内部信号量数据结构的一 个复制。
	ushort *array;//用在GETALL/SET ALL命令中的 一 个指针。它应当指向整数值的一 个数组。在设置或获取集合中所有信号量的值的过程中,将会用到该数组。
	struct seminfo *buf;//信号量内部结构
};

    函数semctl()用于在信号量集合上执行控制操作。这个调用类似于函数msgctl(), msgctl() 函数是用于消息队列上的操作。
        第1个参数是关键字的值(在我们的例子中它是调用semget()函数所返回的值)。
            第2个参数(semun)是将要执行操作的信号量的编号,它是信号量集合的 一 个索引值,对于集合中的第 1个信号量 (有可能只有这一 个信号量)来说,它的索引值将是 一 个为0 的值。

cmd 参数代表将要在集合上执行的命令。其取值如下所述:

    IPC_STAT: 获取某个集合的semid_ds结构,并把它存储在semun联合体的buf参数所指定的地址中。

    IPC_SET:设置某个集合的semid_ds结构的ipc_perm成员的值。该命令所取的值是从semun联合体的buf参数中取到的。

    IPC_RMID : 从内核删除该集合。

    GETALL: 用于获取集合中所有信号量的值。整数值存放在无符号短整数的一个数组中,该数组由联合体的array成员所指定。

    GETNCNT: 返回当前正在等待资源的进程的数目。

    GETPID: 返回最后一 次执行 semop 调用的进程的PID 。

    GETVAL: 返回集合中某个信号量的值。

    GETZCNT: 返回正在等待资源利用率达到百分之百的进程的数目。

    SETALL: 把集合中所有信号量的值,设置为联合体的array成员所包含的对应值。

    SETVAL: 把集合中单个信号量的值设置为联合体的val成员的值。

例:利用semctl()函数设置和获得信号量的值构建同用的函数:

void SetvalueSem(sem_t semid,int value)//设置信号的值,通过SETVAL实现,所设置的值通过联合变量sem的val域实现
{
	union semun sem;//信号量操作的结构
	sem.val = value;//初始化
	semctl(semid,0,SETVAL,sem);//设置信号量的值
}


int GetvalueSem(sem_t semid)//获得信号量的值
{
	union semun sem;//信号量操作的结构
	return semctl(semid,0,GETVAL,sem);//获得信号量的值,通过GETVAL会是其返回给定信号量的当前值
}


void DestroySem(sem_t semid)//销毁信号量
{
	union semun sem;//信号量操作的结构
	sem.val = 0;//初始化
	semctl(semid,0,IPC_RMID,sem);//设置信号量
}

4.信号量操作函数semop()

信号量的P、V操作是通过向语已经建立好的信号量(使用semget()函数),发送命令来完成的。向信号量发送命令的函数是semop(),原型如下:

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


    参数sops是一个指针,指向将要在信号量集合上执行操作的一个数组,参数nsops则是该数组中操作的个数。

    sops参数指向的是类型为sembuf结构的一个数组

struct sembuf{
	ushort sem_num;//信号量的编号
	short  sem_op;//信号量的操作;将要执行的操作(正、负或者零)。
	short sem_flag;//信号量的操作标志;如果sem_op为负,则从信号量中减掉一个值。为正,则从信号量中加上值。如果为0,则将进程设置为睡眠状态,直到信号量的值为0为止。
	};
//例如
	struct sembuf sem= {O, +1, NOWAJT};
//表示对信号量 0,进行加1的操作。

例:用函数semop()可以构建基本的P、V操作,代码如下所示。Sem_P构建{0, +1, NOWAJT} 的sembuf结构来进行增加1个信号量值的操作; Sem_V构建{0, -1, NOWAJT}的sembuf结构来进行减少1个信号量的操作,所对应的信号量由函数传入(semid)。

int Sem_P(sem_t semid) //增加信号量
{
	struct sembuf sops={0,+1,IPC_NOWAIT};//建立信号量结构值
	return (semop(semid,&sops,1));//发送命令
}
int Sem_V(sem_t semid) //减小信号量值
{
	struct sembuf sops = {0,-1,IPC_NOWAIT};//建立信号量结构值
	return (semop(semid,&sops,1));//发送信号量操作方法
}

5.信号例子

#include<stdio.h>
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/sem.h>

typedef int sem_t;

union semun{
        int     val;
        struct semid_ds *buf;
        unsigned short  *array;
}arg;

sem_t CreateSem(key_t key,int value)
{
        union semun sem;
        sem_t semid;
        sem.val = value;

        semid = semget(key,0,IPC_CREAT|0666);
        if(-1 == semid)
        {
                printf("create semaphore error\n");
                return -1;
        }
        semctl(semid,0,SETVAL,sem);
        return semid;
}

int Sem_P(sem_t semid)
{
        struct sembuf sops = {0,+1,IPC_NOWAIT};
        return (semop(semid,&sops,1));
}

int Sem_V(sem_t semid)
{
        struct sembuf sops = {0,-1,IPC_NOWAIT};
        return (semop(semid,&sops,1));
}

void SetvalueSem(sem_t semid,int value)
{      union semun sem;
        sem.val = value;
        semctl(semid,0,SETVAL,sem);
}

int GetvalueSem(sem_t semid)
{
        union semun sem;
        return semctl(semid,0,GETVAL,sem);
}


void DestroySem(sem_t semid)
{
        union semun sem;
        sem.val = 0;
        semctl(semid,0,IPC_RMID,sem);
}


int main(void)
{
        key_t key;
        int semid;
        char i;
        int value = 0;
        key = ftok("/ipc/sem",'a');
        semid = CreateSem(key,100);
        for(i=0;i<=3;i++){
                Sem_P(semid);
                Sem_V(semid);
        }
        value = GetvalueSem(semid);
        printf("信号量为:%d\n",value);
        DestroySem(semid);
        return 0;
}

四、多个阻塞信号量操作的处理

    因减少一个信号量值而发生阻塞的进程对该信号量减去的值是一样的,那么当条件允许是到底哪个进程会首先被允许执行操作是不确定的。

        若多个因减少一个信号量而发生阻塞的进程对该信号量减去的值是不同的,那么会按照先满足条件先满足的顺序来进行:

	 		1. 若一个信号量的当值为 0,进程 A 请求将信号量值减去 2,然后进程 B 请求将信号量值减去 1。如果第三个进程将信号量值加上了 1,那么进程 B 首先会被解除阻塞并执行它的操作
			
			2. 这种场景可能会导致饿死情况的发生,即一个进程因信号量的状态无法满足所请求的操作继续往前执行的条件而永远保持阻塞。

当一个进程因试图在多个信号量上执行操作而发生阻塞时也可能会出现饿死的情况。考虑下面的这些在一组信号量上执行的操作,两个信号量的初始值都为 0

  		1. 进程 A 请求将信号量 0 和 1 的值减去 1(阻塞)。
  		2. 进程 B 请求将信号量 0 的值减去 1(阻塞)
  		3. 进程 C 将信号量 0 的值加上 1

五、信号量撤销值

    问题:若进程在调用完信号量值之后主动或被动的终止了,可能会给其他使用这个信号量的进程带来问题,因为它们可能因等待这个信号量而被阻塞着——即等待已经被终止的进程撤销对信号量所做的变更。

        解决:在通过semop()修改一个信号量值时可以使用SEM_UNDO标记。当指定这个标记时,内核会记录信号量操作的效果,然后在进程终止(主动或被动)时撤销这个操作。

            使用semctl SETVAL或者SETALL操作设置一个信号量时,所有使用这个信号量的进程中相应的 semadj 会被清空(即设置为 0)。

            fork()创建的子进程不会继承其父进程的 semadj 值,因为对于子进程来讲撤销其父进程的信号量操作毫无意义。另一方面,semadj 值会在 exec()中得到保留。这样就能在使用SEM_UNDO 调整一个信号量值之后通过 exec()执行一个不操作该信号量的程序,同时在进程终止时原子地调整该信号量。(这项技术可以允许另一个进程发现这个进程何时终止。)

六、信号量初始化竞争条件

    当实际操作创建一个新的信号量集时,semget会将该集合中各个信号量的值初始化为0,但有些系统却不能保证做到。

        早期的System V实现根本不对信号量值进行初始化,存放新创建信号量集的那部分内存空间最近一次使用时的值就是各个信号量的初始值。
            X/Open XPG3可移植性指南和Unix98纠正了这个忽略行为,明确地陈述semget并不初始化各个信号量的值,这个初始化必须通过以SET_VAL命令(设置集合中一个值)或SETALL命令(设置集合中所有值)调用semctl来完成。

System V信号量的设计中,创建一个信号量集(semget)并将它初始化(semctl)需两次函数调用是一个致命的缺陷,容易产生竞争状态。

一个不完备的解决方案是:

    在调用semget时指定IPC_CREAT| IPC_EXCL标志,这样只有一个进程(首先调用semget的那个进程)创建所需信号量。
    该进程随后初始化该信号量,其他进程会收到来自semget的一个EEXIST错误,于是再次调用semge,不过这次调用既不指定IPC_CREAT标志,也不指定IPC_EXCL标志。

但是竞争状态任然存在。两个进程几乎同时尝试创建初始化一个只有单个成员的信号量集,执行代码如下:

oflag = IPC_CREAT|IPC_EXCL|SVSEM_MODE;
if((semid = semget(key,1,oflag))>0){
	arg.val = 1;
	semctl(semid,0,SETVAL,arg);
	}else if(errno == EEXIST){
	semid = semget(key,1,SVSEM_MODE);
}else
err_sys("semget error");
semop(semid,...);

发生情形:

    第一个进程执行1-3行,然后内核阻止指行。
    内核启动第二个进程,执行1、2、5、6、9行。

    尽管成功创建该信号量的第一个进程将是初始化该信号量的唯一进程,但是由于它完成创建和初始化操作需花两个步骤,因此内核有可能在这两个步骤之间把上下文切换到另一个进程。
        切换的进程随后可以使用该信号量,但是该信号量的值尚未由第一个进程初始化。当第二个进程执行第9行时,该信号量的值是不确定的。

绕过这个竞争状态的方法:

    当semget创建一个新的信号量集时,其semid_ds结构的s_otime成员保证被置为0。该成员只是在semop调用成功时才被设置为当前值。

    因此,上面例子中的第二个进程再次成功地调用semget 后,必须以IPC_STAT命令调用semctl。

    然后等待sem_otime变为非零值,到时就可断定该信号赁已被初始化,而且对它进行初始化的郑个进程已成功地调用sernop。

    意味着创建该信号量的那个进程必须初始化它的值,而且必须在任何其他进程可以使用该信号量之前调用semop。

例:若一个应用程序由多个平等的进程构成,这些进程使用一个信号量来协调相互之间的动作。由于无法保证哪个进程会首先使用信号量量(这就是地位平等的含义),因此每个进程都要做好信号量不存在时创建和初始化信号量的准备。错误的演示:

#define _GNU_SOURCE
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <zconf.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <sys/stat.h>
#include <errno.h>
#include <cstdarg>
#include <sys/types.h>          /* For portability */
#include <sys/sem.h>
#include <time.h>
#include <memory.h>
union semun {                   /* Used in calls to semctl() */
    int                 val;
    struct semid_ds *   buf;
    unsigned short *    array;
#if defined(__linux__)
    struct seminfo *    __buf;
#endif
};

int main(int argc, char *argv[])
{
    int semid, key, perms;
    struct sembuf sops[2];

    key = 12345;
    perms = S_IRUSR | S_IWUSR;
    semid = semget(key, 1, IPC_CREAT | IPC_EXCL | perms);

    if (semid != -1) {                  /* Successfully created the semaphore */
        union semun arg;

        /* XXXX */

        arg.val = 0;                    /* So initialize it */
        if (semctl(semid, 0, SETVAL, arg) == -1){
            perror("semctl");
            exit(EXIT_FAILURE);
        }
            

    } else {                            /* We didn't create semaphore set */
        if (errno != EEXIST) {          /* Unexpected error from semget() */
            perror("semget 1");
            exit(EXIT_FAILURE);
        } else {                        /* Someone else already created it */
            semid = semget(key, 1, perms);      /* So just get ID */
            if (semid == -1){
                perror("semget 2");
                exit(EXIT_FAILURE);
            }
        }
    }

    /* Now perform some operation on the semaphore */

    sops[0].sem_op = 1;         /* Add 1 */
    sops[0].sem_num = 0;        /* ... to semaphore 0 */
    sops[0].sem_flg = 0;
    if (semop(semid, sops, 1) == -1){
        perror("semop");
        exit(EXIT_FAILURE);
    }
        

    exit(EXIT_SUCCESS);
}

上面代码问题在于:首先,进程 B在一个未初始化的信号量(即其值是一个任意值)上执行了一个 semop()。其次,进程 A 中的 semctl()调用覆盖了进程 B 所做出的变更。
解决方案依赖于一个现已成为标准的特性,即与这个信号量集相关联的semid_ds 数据结构中的 sem_otime 字段的初始化。
在一个信号量集首次被创建时,sem_otime字段会被初始化为 0,并且只有后续的 semop()调用才会修改这个字段的值。
根据这个特性来消除上面描述的竞争条件,即只需要插入额外的代码来强制第二个进程(即没有创建信号量的那个进程)等待,直到第一个进程既初始化了信号量又执行了一个更新 sem_otime字段但不修改信号量的值的 semop()调用为止。

#define _GNU_SOURCE
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <zconf.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <sys/stat.h>
#include <errno.h>
#include <cstdarg>
#include <sys/types.h>          /* For portability */
#include <sys/sem.h>
#include <time.h>
#include <memory.h>
union semun {                   /* Used in calls to semctl() */
    int                 val;
    struct semid_ds *   buf;
    unsigned short *    array;
#if defined(__linux__)
    struct seminfo *    __buf;
#endif
};

int
main(int argc, char *argv[])
{
    int semid, key, perms;
    struct sembuf sops[2];

    if (argc != 2 || strcmp(argv[1], "--help") == 0){
        printf("%s sem-op\n", argv[0]);
        exit(EXIT_FAILURE);
    }
        

    key = 12345;
    perms = S_IRUSR | S_IWUSR;

    semid = semget(key, 1, IPC_CREAT | IPC_EXCL | perms);
    if (semid != -1) {                  /* Successfully created the semaphore */
        union semun arg;
        struct sembuf sop;

        sleep(5);
        printf("%ld: created semaphore\n", (long) getpid());

        arg.val = 0;                    /* So initialize it to 0 */
        if (semctl(semid, 0, SETVAL, arg) == -1){
            printf("semctl 1");
            exit(EXIT_FAILURE);
        }
           
        printf("%ld: initialized semaphore\n", (long) getpid());

        /* Perform a "no-op" semaphore operation - changes sem_otime
           so other processes can see we've initialized the set. */

        sop.sem_num = 0;                /* Operate on semaphore 0 */
        sop.sem_op = 0;                 /* Wait for value to equal 0 */
        sop.sem_flg = 0;
        if (semop(semid, &sop, 1) == -1){
            printf("semop");
            exit(EXIT_FAILURE);
        }
            
        printf("%ld: completed dummy semop()\n", (long) getpid());

    } else {                            /* We didn't create the semaphore set */

        if (errno != EEXIST) {          /* Unexpected error from semget() */
            printf("semget 1");
            exit(EXIT_FAILURE);
        } else {                        /* Someone else already created it */
            const int MAX_TRIES = 10;
            int j;
            union semun arg;
            struct semid_ds ds;

            semid = semget(key, 1, perms);      /* So just get ID */
            if (semid == -1){
                printf("semget 2");
                exit(EXIT_FAILURE);
            }
                

            printf("%ld: got semaphore key\n", (long) getpid());
            /* Wait until another process has called semop() */

            arg.buf = &ds;
            for (j = 0; j < MAX_TRIES; j++) {
                printf("Try %d\n", j);
                if (semctl(semid, 0, IPC_STAT, arg) == -1){
                    printf("semctl 2");
                    exit(EXIT_FAILURE);
                }
                    

                if (ds.sem_otime != 0)          /* Semop() performed? */
                    break;                      /* Yes, quit loop */
                sleep(1);                       /* If not, wait and retry */
            }

            if (ds.sem_otime == 0)              /* Loop ran to completion! */
            {
                printf("Existing semaphore not initialized");
                exit(EXIT_FAILURE);
            }
        }
    }

    /* Now perform some operation on the semaphore */

    sops[0].sem_num = 0;                        /* Operate on semaphore 0... */
    sops[0].sem_op = atoi(argv[1]);
    sops[0].sem_flg = 0;
    if (semop(semid, sops, 1) == -1){
        printf("semop");
        exit(EXIT_FAILURE);
    }
        

    exit(EXIT_SUCCESS);
}

离线

页脚

Powered by FluxBB

本站由XREA提供空间支持