Linux进程同步之System V 信号量

System V信号量是不属于POSIX标准,它属于SUSSingle UNIX Specification)单一UNIX规范中的扩展定义。它和POSIX信号量一样都提供基本的信号量功能操作。

System V信号量相对于POSIX信号量最大的区别是在信号量的操作复杂度

POSIX信号量中说过,根据信号量取值(代表可用资源的数目)的不同,POSIX信号量可以分为:

  • 二值信号量:信号量的值只有01,这和互斥量很类型,若资源被锁住,信号量的值为0,若资源可用,则信号量的值为1
  • 计数信号量:信号量的值在0到一个大于1的限制值(POSIX指出系统的最大限制值至少要为32767)。该计数表示可用的资源的个数。

相对于POSIX信号量,System V信号量增加了复杂度,我们称System V信号量之为:计数信号量集。计数信号量集:至少有一个信号量构成的集合,其中集合中的每个信号量都是计数信号量。对于信号量集中的信号量数目系统内核是存在限制的,由内核参数:SEMMSL决定。

[root@localhost ~]# grep SEMMSL -R /usr/include/*
/usr/include/linux/sem.h:#define SEMMSL  250  /* <= 8 000 max num of semaphores per id */
... ...

对于System V信号量,系统内核还有很多限制,譬如下面:

SEMMNS:系统中信号量的最大数目,等于SEMMNI*SEMMSL
SEMOPM:一次semopt()操作的最大信号量数目
SEMMNI。系统内核中信号量的最大数目
对于系统中的每个 System V 信号量,即每个信号量集,内核都会维护一个 semid_ds 的信息结构,如下是Linux 2.6.18下的定义 :
/* Data structure describing a set of semaphores.  */
struct semid_ds
{
  struct ipc_perm sem_perm;            // IPC的操作权限,每个IPC结构都有
  __time_t sem_otime;                  //上一次执行semop() 的时间
  unsigned long int __unused1;         //预留使用
  __time_t sem_ctime;                  //上一次通过semctl()进行的修改时间
  unsigned long int __unused2;
  unsigned long int sem_nsems;         //信号量集中的信号量数目
  unsigned long int __unused3;
  unsigned long int __unused4;
};

在《APUEP423提到,信号量集中每一个信号量由一个无名结构表示。但是我始终无法找到该结构体的定义书中提到该结构的定义如下:

struct {
    unsigned short semval;    //信号量的值
    pid_t  sempid;            //最后一次semctl操作的进程id
    unsigned short semncnt;   //等待semval变为大于当前值的线程数
    unsigned short semzcnt;   //等待semval变为0的线程数

};

1 System V信号量的创建和打开

#include <sys/sem.h>
int semget(key_t key, int nsems, int semflg);
                         //若成功就返回飞非负的标识符,否则返回-1

semget用于创建或打开一个已存在的信号量。

key用于生成唯一信号量的key,主要的目的是使不同进程在同一该IPC汇合。key可以是事先不同的进程约定好的一个值,也可以不同进程通过相同的路径名和项目ID,调用ftok()函数,生成一个键。

nsems:表示信号量集中信号量的个数,如果创建一个信号量集,nsems必须是一个非0正整数,如果打开一个指定的信号量集,nsems可以指定为0

semflagIPC_CREAT, IPC_EXCL,以及IPC的指定权限位。如果为IPC_CREATIPC_EXCL,当该信号量集以及存在会返回错误。errnoEEXIST

semget的返回值是被称为信号量标识符的整数,semopsemctl函数将通过该标识符对信号量集进行操作。

这里需要知道是调用semget()创建一个新的信号量集并没有对之初始化,需要调用后面要讲的semctl()函数进行初始化。这样System V信号量的创建和初始化就不是一个原子操作,这是一个很大的缺陷。会出现使用未初始化信号量集的问题。

2 System V信号量的控制操作

#include <sys/sem.h>
int semctl(int semid, int semnum, int cmd, .../* union semun arg*/);
                         //若失败返回-1,并设置errno,成功具体返回值如下

semctl函数主要是对信号量集的一系列控制操作,根据操作命令cmd的不同,执行不同的操作,依赖于所请求的命令,第四个参数是可选的,

semidSystem V信号量的标识符;

semnum:表示信号量集中的第semnum个信号量。它的取值范围:0~nsems-1

cmd:操作命令;

arg:如果使用该参数,该参数的类型为 union semun,它是多个特定命令的联合。按照SUS明确规定,这个结构必须有用户自己定义,在Linux 2.6.18的系统头文件中也没有该结构的定义,但在<bits/sem.h>中有一段对该结构的定义的建议,不过是注释掉的。

include <bits/sem.h>

/* The user should define a union like the following to use it for 
                 arguments for `semctl'.
union semun {
               int  val;                // Value for SETVAL
               struct semid_ds *buf;    // Buffer for IPC_STAT, IPC_SET
               unsigned short  *array;  // Array for GETALL, SETALL
               struct seminfo  *__buf;  // Buffer for IPC_INFO(Linux-specific)
};
*/

semctlcmd命令有10种,如下:

  • IPC_STAT:获取此信号量集合的semid_ds结构,存放在第四个参数argbuf中;
  • IPC_SET:通过arg.buf来设定信号量集相关联的semid_ds中信号量集合权限为sem_perm中的uidgidmode
  • IPC_RMID:从系统中删除该信号量集合。这种删除立即发生,仍在使用该信号量集的其他进程,在下次对该信号量集进行操作的时候,会发生错误并返回EIDRM。这和POSIX信号量是不一样的。POSIX信号量sem_unlink只是会立即删除信号量的在文件系统中的文件,而信号量的析构是在最后一个sem_close发生是进行的。
  • GETVAL:返回第semnum个信号量的值;
  • SETVAL:设置第semnum个信号量的值,该值由第四个参数arg中的val指定;
  • GETPID:返回第semnum个信号量的sempid,最后一个操作的pid
  • GETNCNT:返回第semnum个信号量的semncnt。等待semval变为大于当前值的线程数;
  • GETZCNT:返回第semnum个信号量的semzcnt。等待semval变为0的线程数。
  • GETALL:去信号量集合中所有信号量的值,将结果存放到arg中的array所指向的数组。
  • SETALL:按arg.array所指向的数组中的值,设置集合中所有信号量的值。

对于GETALL以外的所有GET命令,semctl都返回相应的值,其他命令的返回值为0

3 System V信号量的信号量操作

#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);
                                          //成功返回0,出错返回-1

semop函数主要是在已打开的信号量集上,对其中的一个或多个信号量的值进行操作

semidSystem V信号量的标识符,用来标识一个信号量集。

sops:是指向一个struct sembuf结构体数组的指针,该数组是一个信号量操作数组。

nsopssops所指向sembuf结构体数组中元素的个数。

sembuf结构体的定义如下:

struct sembuf
{
  unsigned short int sem_num;   /* 信号量的序号从0~nsems-1 */
  short int sem_op;            /* 对信号量的操作,>0, 0, <0 */
  short int sem_flg;            /* 操作标识:0, IPC_WAIT, SEM_UNDO */
};

sem_num标识信号量集中的第几个信号量,0表示第1个,1表示第2个,nsems - 1表示最后一个。

sem_op标识对信号量的所进行的操作类型。对信号量的操作有三种类型:

  • sem_op > 0,对该信号量执行挂出操作,挂出的值由sem_op决定,系统会把sem_op的值加到该信号量的当前值semval(参考文章开头关于每个信号量结构的定义)上。如果sem_flag指定了SEM_UNDO(还原)标志,那么相应信号量的semadj值会减掉sem_op的值。下面会说明semadj的含义。
  • sem_op < 0,对该信号量执行等待操作,当信号量的当前值semval >= -sem_op时,semval减掉sem_op的绝对值,为该线程分配对应数目的资源。如果指定SEM_UNDO,相应信号量的semadj就加上sem_op的绝对值。当semval < -sem_op时,相应信号量的semncnt就加1,调用线程被阻塞,直到semval >= -sem_op,当此条件满足时,调用线程被唤醒,执行相应的分配操作,然后semncnt减去1.
  • sem_op = 0,表示调用者希望semval变为0。如果为0则立即返回,如果不为0,相应信号量的semzcnt1,调用调用线程被阻塞。

sem_flag:信号量操作的属性标志,如果为0,表示正常操作,如果为IPC_WAIT,使对信号量的操作时非阻塞的。即指定了该标志,调用线程在信号量的值不满足条件的情况下不会被阻塞,而是直接返回-1,并将errno设置为EAGAIN。如果为SEM_UNDO,那么将维护进程对信号量的调整值,以便进程结束时恢复信号量的状态。

下面解释一下与单个信号量相关的几个值:

semval:信号量的当前值,在文章开头信号量的结构中已提到。

semncnt:等待semval变为大于当前值的线程数。在文章开头信号量的结构中已提到。

semzcnt:等待semval变为0的线程数。在文章开头信号量的结构中已提到。

semadj:指定信号量针对某个特定进程的调整值。只有sembuf结构的sem_flag指定为SEM_UNDO后,semadj才会随着sem_op而更新。讲简单一点:对某个进程,在指定SEM_UNDO后,对信号量semval值的修改都会反应到semadj上,当该进程终止的时候,内核会根据semadj的值,重新恢复信号量之前的值

这里我们可以看到System V信号量可以对信号量集中的某一信号量进行加减sem_op,该值不仅仅为1,而在POSIX信号量中只能对信号量进行加减1的操作。

4 System V信号量的继承和销毁

(1)继承

POSIX的有名信号量一样,父进程中打开的信号量在子进程中仍然是保持着打开状态的。

(2)销毁

  1. 对于一个持有该信号量的进程在没有释放该信号量的情况下就终止了。那么该进程所占用的信号量内核是不会进行释放的。
  2. 当通过semctl传入IPC_RMID对该信号量集进行删除时,会立即将该信号量从系统中彻底删除,不能再对该信号量进行任何访问。这和POSIX信号量是不一样的。

下面代码进行了测试:

#include <iostream>
#include <fstream>
#include <cstdlib>
#include <cstring>

#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <sys/ipc.h>
#include <sys/sem.h>

using namespace std;

#define SEM_PATHNAME "/tmp/sem_name"

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

int CreateKey(const char * pathName)
{
    int fd = open( pathName, O_CREAT , 0666);

    if (fd < 0)
    {
        cout<<"open file error..."<<strerror(errno)<<endl;
        return -1;
    }
    close(fd);

    return ftok(pathName, 0);
}

int main()
{
   int semId;
   semun arg;

   //解决信号量的创建和初始化不是原子操作的一种方案
   if ((semId = semget(CreateKey(SEM_PATHNAME), 1, IPC_CREAT | IPC_EXCL | 0666)) >= 0)
   {
       arg.val = 4;

       if (semctl(semId, 0, SETVAL, arg) < 0)
       {
           cout<<"semctl error "<<strerror(errno)<<endl;
           return -1;
       }
   }
   else if (errno == EEXIST)
   {
       semId = semget(CreateKey(SEM_PATHNAME), 1, 0666);
   }
   else
   {
       cout<<"semget error "<<strerror(errno)<<endl;
       return -1;
   }

   cout<<"parent:sem value:"<<semctl(semId, 0, GETVAL)<<endl;

   if (fork() == 0)
   {
       struct sembuf buffer;
       buffer.sem_num = 0;
       buffer.sem_op = -2;
       buffer.sem_flg = 0;

       semop(semId, &buffer, 1);

       cout<<"child:sem value:"<<semctl(semId, 0, GETVAL)<<endl;
       exit(0);
   }

   sleep(1);
   cout<<"parent:sem value:"<<semctl(semId, 0, GETVAL)<<endl;
}
执行结果如下:
parent:sem value:4
child:sem value:2
parent:sem value:2

下面代码测试信号量集的删除:

int main()
{
   int semId;
   semun arg;

   //解决信号量的创建和初始化不是原子操作的一种方案
   if ((semId = semget(CreateKey(SEM_PATHNAME), 1, IPC_CREAT | IPC_EXCL | 0666)) >= 0)
   {
       arg.val = 4;
       if (semctl(semId, 0, SETVAL, arg) < 0)
       {
           cout<<"semctl error "<<strerror(errno)<<endl;
           return -1;
       }
   }
   else if (errno == EEXIST)
   {
       semId = semget(CreateKey(SEM_PATHNAME), 1, 0666);
   }
   else
   {
       cout<<"semget error "<<strerror(errno)<<endl;
       return -1;
   }
   cout<<"parent:sem value:"<<semctl(semId, 0, GETVAL)<<endl;

   if (fork() == 0)
   {
       semctl(semId, 0, IPC_RMID);
       exit(0);
   }
   sleep(1);

   if (semctl(semId, 0, GETVAL) == -1)
   {
       cout<<"semctl error:"<<strerror(errno)<<endl;
       return -1;
   }
}

测试结果如下:

parent:sem value:4
semctl error:Invalid argument

5 System V信号量的测试 

下面是测试代码,信号量集中只有一个信号量,该信号量的值被设成6,每次semop获取两个信号量才能对共享资源(这里用文件代替)进行访问;

#include <iostream>
#include <fstream>
#include <cstdlib>
#include <cstring>

#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <sys/ipc.h>
#include <sys/sem.h>

using namespace std;

#define SEM_PATHNAME "/tmp/sem_name"

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

int CreateKey(const char * pathName)
{
    int fd = open( pathName, O_CREAT , 0666);

    if (fd < 0)
    {
        cout<<"open file error..."<<strerror(errno)<<endl;
        return -1;
    }

    close(fd);
    return ftok(pathName, 0);
}

void semTest(int flag, int semId)
{ 
    struct sembuf buffer;
    buffer.sem_num = 0;
    buffer.sem_op = -2;
    buffer.sem_flg = 0;

    semop(semId, &buffer, 1);

    ofstream fileStream("./test.txt", ios_base::app); 
 
    for (int i = 0; i < 5; ++i)  
    {  
        sleep(1);  
        fileStream<<flag;  
        fileStream<<' '<<flush;  
    }  

    buffer.sem_op = 2;
    semop(semId, &buffer, 1);
}

int main()
{
   int semId;
   semun arg;

   //解决信号量的创建和初始化不是原子操作的一种方案
   if ((semId = semget(CreateKey(SEM_PATHNAME), 1, IPC_CREAT | IPC_EXCL | 0666)) >= 0)
   {
       arg.val = 4;
       if (semctl(semId, 0, SETVAL, arg) < 0)
       {
           cout<<"semctl error "<<strerror(errno)<<endl;
           return -1;
       }
   }
   else if (errno == EEXIST)
   {
       semId = semget(CreateKey(SEM_PATHNAME), 1, 0666);
   }
   else
   {
       cout<<"semget error "<<strerror(errno)<<endl;
       return -1;
   }

   for (int i = 0; i < 3; ++i)
   {
       if (fork() == 0)
       {
            semTest(i + 1, semId);
            sleep(1);

            exit(0);
       }
   }
}
执行结果如下:

./test.txt
1 2 1 2 1 2 2 1 1 2 3 3 3 3 3


Jul 4, 2013 AM 00:26 @dorm

有想法就要努力的去尝试---skywalker

你可能感兴趣的:(linux,System,进程同步,信号量,xsi,v)