【Linux进程间通信】 - 信号量

原创作品,转载请标明:http://blog.csdn.net/Xiejingfa/article/details/50904927

前面我们在介绍共享内存(传送门: 【Linux进程间通信】 - 共享内存)这种进程间通信时方式时提到,使用共享内存通信时需要使用同步机制来控制多个进程对同一内存区的读写操作。今天我们就来讲述一种常用的多进程同步机制 – 信号量。

1、什么是信号量?

在多进程环境下,为了防止多个进程同时访问一个公共资源而出现问题,需要一种方法来协调各个进程,保证它们能够合理地使用公共资源。信号量就是这样一种机制。信号量可以用来保证两个或多个临界区代码不被并发调用。具体过程是:一个进程在执行临界区代码前,需要获取一个信号量,一旦该临界区代码执行完毕,那么该进程必须释放信号量。其它想进入该临界区代码的进程也必须等待直到获取到一个信号量。信号量分为单值信号量和多值信号量,单值信号量只能被一个进程获取,多值信号量可以被多个进程获取。

上面的介绍好像有点抽象,我们举一个停车场的例子来详细说明一下。假设有一个停车场,只有两个车位(即信号量的初始值为2,表示有两个同样的公共资源)。某个时刻,有三辆车同时来到停车场(即三个并发进程)。由于停车场只有两个空的停车位,所以保安只能让先来的前两辆车进入停车场(即获得信号量),第三辆车只能等待(未能获得信号量,一直等待)。等了一会儿后,有一辆车离开了(释放信号量),这时停车场有一个空车位,第三辆车就可以进入停车场了(获取信号量,进入临界区)。

对比上面这个例子,信号量是一个特殊的非负整数,表示某类公共资源的数量。如果一个进程占用了该资源,会将信号量的值做减1操作,相反,一个进程使用完毕该资源则对信号量做加1操作,如果该信号量的数值为0,则所有试图使用该类资源的进程都必须等待直到信号量值不为0。上面这种加1操作和减1操作都是原子操作,前者又称为P操作,后者又称为V操作。

关于信号量,一定要弄明白:信号量是一种“锁”的概念,它本身不具备数据传输的功能,而是通过对进程访问资源时进行同步控制来实现进程间通信。

2、信号量PV操作

信号量只有两种操作,P操作和V操作。具体如下:

  • P操作:如果信号量的值大于0,则做减1操作,表示进程获得该资源的使用权。否则进程进入休眠状态直到信号量的值大于0后再被唤醒。
  • V操作:表示该进程不再使用信号量控制的资源,对信号量做加1操作。如果此时有其它进程在等待该信号量,则唤醒它们。

为了保证信号量能有效工作,信号量的测试以及加1和减1操作都必须是原子性的。所以,信号量通常都是在内核中实现的。

3、信号量的相关操作

在Linux中,对信号量进行操作的主要有semget、semop、semctl三个函数,它们都被定义在<sys/sem.h>头文件中。值得注意的是,Linux在实现上可以同时对多个信号量进行控制,这些成组的信号量又称为信号量集。

在介绍这些函数之前,我们先来讲述一下信号量在内核中的表示。

内核为每个信号量集维护这样一个semid_ds结构(摘自网络):

struct semid_ds
{
    struct ipc_perm sem_perm;  /* operation permission struct */
    struct sem *sem_base;      /* ptr to first semaphore in set */
    unsigned short sem_nsems;  /* numbers of semaphores in set */
    time_t sem_otime;          /* last semop time */
    time_t sem_ctime;          /* last change time */
    ...
};

每个信号量又由一个sem结构表示(摘自网络):

struct sem
{
    unsigned short semval;   /* semaphore value, always >= 0 */
    pid_t sempid;            /* pid for last operation */
    unsigned short semncnt;  /* number of processes awaiting semval > vurrval */
    unsigned short semzcnt;  /* number of processes awaiting semval = 0 */
    ...
};

下面具体介绍一下这些函数。

3.1、创建或打开一个信号量集

使用semget函数可以创建或打开一个信号量集,函数原型如下:

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

int semget(key_t key, int nsems, int semflg);

如果操作成功,semget函数返回信号量集的标识符ID,否则返回-1。各个参数的含义具体如下:

(1)第一个参数key和第三个参数semflg

参数key表示所要创建或打开的信号量集对应的键值,参数semflg指明了调用函数的操作类型。这两个参数跟【Linux进程间通信】 - 共享内存一文中介绍的shmget函数一致,这里就不再赘述。

(2)、第二个参数nsems

参数nsems表示创建的信号量集中包含的信号量的个数,通常被设置为1。这个参数只有在创建一个新的信号量集时才有效。

当semget成功创建一个新的信号量集时,它所关联的semid_ds结构被初始化。

3.2、信号量集的操作

使用semop函数可以操作一个信号量集,原型如下:

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

 int semop(int semid, struct sembuf *sops, unsigned nsops);

如果操作成功,semop函数返回0,否则返回-1。各个参数的含义具体如下:

(1)、第一个参数semid

参数semid表示需要操作的信号量集的标识符ID,也就是通过semget函数返回的标识符ID。

(2)、第二个参数sops

参数sops用来说明需要执行的操作。其定义如下:

struct sembuf
{
    unsigned short sem_num;
    short sem_op;
    short sem_flg;
};

在sembuf结构中,sem_num表示该信号量集中的某一个信号量(即将要操作的信号量)。参数sem_op表示要执行的操作,参数sem_flg说明了函数semop的行为,取值通常为IPC_NOWAIT和SEM_UNDO。参数sem_op不同值对应的操作如下:

1、如果sem_op > 0时,表示进程对资源使用完毕,释放所占用的资源并将sem_op加到信号量的值上,此时可用资源数量增加。

2、如果sem_op = 0时,表示调用进程阻塞直到信号量相应的值为0。
此时需要分情况讨论:
如果当前信号量的值已经为0,函数立即返回。
如果当前信号量的值不为0,则由sem_flg参数决定函数动作:
a、如果sem_flg被指定为IPC_NOWAIT,则出错返回EAGAIN。
b、如果sem_flg未指定为IPC_NOWAIT,则该信号量semzcnt值加1,调用进程进入休眠状态,直到下列事情发生:
i、此信号量为0,则对semzcnt值减1,表示调用进程已经结束等待;
ii、此信号量被删除,函数出错返回EIDRM;
iii、进程捕获到一个信号并从信号处理函数返回。在这种情况下semzcnt值减1,函数出错返回EINTR。

3、如果sem_op < 0 时,表示调用进程请求|sem_op|(绝对值)数量的资源。此时需要分情况讨论:
如果相应的资源数可以满足请求,则将信号量的值减去semp_op的绝对值,函数成功返回。
如果相应的资源不能满足要求,则由sem_flg参数决定函数动作:
a、如果sem_flg被指定为IPC_NOWAIT,则函数出错返回EAGAIN。
b、如果sem_flg未被指定为IPC_NOWAIT,则该信号量semncnt值加1,调用进程进入休眠状态,直到下列事情发生:
i、当相应的资源数量可以满足要求,则将信号量的值减去semp_op的绝对值,函数成功返回。
ii、此信号量被删除,函数出错返回EIDRM;
iii、进程捕获到一个信号并从信号处理函数返回。在这种情况下semncnt值减1,函数出错返回EINTR。

(3)、第三个参数nops

参数nops表示sops数组的元素个数。

3.3、信号量集的控制

共享内存有自己的控制函数shmctl,信号量集也有自己的专用控制函数,原型如下:

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

 int semctl(int semid, int semnum, int cmd, union senun arg);

如果操作成功,semctl返回所请求的值(依据cmd参数而定),否则返回-1。各参数的含义如下:

(1)、第一个参数semid

参数semid表示需要操作的信号量集的标识符ID,也就是通过semget函数返回的标识符ID。

(2)、第二个参数semnum

参数semnum和前面介绍的sembuf结构中的sem_num字段含义一致。

(3)、第四个参数arg

这里有必要先介绍一下第四个参数,第四个参数arg是可选的,是否使用取决参数cmd。

semun结构定义如下:

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

(4)、第三个参数cmd

cmd用来指定下面命令中的一个:

cmd取值 含义
GETALL 获取semid信号集中信号量的个数并赋值给semun.array
SETALL 用semun.array的值设置信号量集中信号量的个数
GETVAL 获取semid信号量集中semnum指定信号量的值semval
SETVAL 用semun.val的值设置信号量集中semnum指定信号量的semval
GETNCNT 获取semnum指定信号量的semncnt值
GETZCNT 获取semnum指定信号量的semzcnt值
IPC_STAT 取该信号集的semid_ds结构,并存放在semun.buf字段中
IPC_SET 按照semun.buf的值设置该信号量集中的sem_perm.uid、sem_perm.gid和sem_perm.mode字段
IPC_RMID 删除信号量集,这种删除是立即发生的
GETPID 获取semnum指定信号量的sempid值

4、示例演示

下面用一个例子来演示一下信号量如何进行同步控制。

sem_test.c代码如下:

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <unistd.h>
#include <time.h>

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


int sem_id = -1;

struct sembuf sem_p = {0, -1, 0};
struct sembuf sem_v = {0, 1, 0};

// P操作
static int P()
{
    return semop(sem_id, &sem_p, 1);
}

// V操作
static int V()
{
    return semop(sem_id, &sem_v, 1);
}

// 打印当前时间,供调试用
static void printCurrentTime()
{
    time_t timep;
    time(&timep);
    printf("%s\n", ctime(&timep));
}

int main(int argc, char *argv[])
{
    // 打开一个信号量集,该信号量集只有一个信号量
    sem_id = semget((key_t) 1234, 1, IPC_CREAT | 666);
    if (sem_id == -1)
    {
        perror("semget error!");    
        exit(1);
    }

    // 设置信号量的初始值
    if (argc > 1)
    {
        union semun semopt;
        semopt.val = 1;
        int res = semctl(sem_id, 0, SETVAL, semopt);
        if (res == -1)
        {
            printf("semctl error!\n");
            exit(1);
        }
    }

    // 准备进入临界区
    printf("before critical section...\n");
    printCurrentTime();
    if (P() == -1)
    {
        printf("P error!\n");
        exit(1);
    }

    // 在临界区里面
    printf("in critical section...\n");
    printCurrentTime();

    sleep(20);

    // 离开临界区
    if (V() == -1)
    {
        printf("V error!\n");
        exit(1);
    }

    printf("after critical section...\n");
    printCurrentTime();
}

第一次运行程序的时候,我们需要初始化信号量的值为1,所以需要以./sem_test 1命令(参数随意,只要保证有两个以上参数即可)运行程序,我们称其为程序A。此时,输出如下:

Wed Mar 16 20:01:52 2016

in critical section...
Wed Mar 16 20:01:52 2016

这时候程序A休眠20秒。

接下来,我们打开另一个终端,输入./sem_test命令运行同一个程序,我们称其为程序B。此时输出如下:

before critical section...
Wed Mar 16 20:01:57 2016

可以看到程序B没有获得信号量,一直阻塞到程序A释放掉信号量。

程序A的完整打印信息如下:

before critical section...
Wed Mar 16 20:01:52 2016

in critical section...
Wed Mar 16 20:01:52 2016

after critical section...
Wed Mar 16 20:02:12 2016

程序B的完整打印信息如下:

before critical section...
Wed Mar 16 20:01:57 2016

in critical section...
Wed Mar 16 20:02:12 2016

after critical section...
Wed Mar 16 20:02:32 2016

参考:《Unix环境高级编程》

你可能感兴趣的:(ipc,信号量,进程间通信)