进程间通信(6)——信号量

0. 前言

通过前面几篇博文,大概清楚这些IPC 的设计都是为了进程间数据的共享而设计的,例如消息队列共享内存命名管道(FIFO),本文将要介绍的信号量(semaphore) 跟这些有些区别,更确切说它是为了共享数据的访问服务,它是一个计数器,是由狄克斯特拉提出,并通过PV(通过&释放,是荷兰文缩写)操作对信号量进行控制。

信号量(semaphore)的数据结构为一个值和一个指针,指针指向等待该信号量的下一个进程。信号量的值与相应资源的使用情况有关。当它的值大于0时,表示当前可用资源的数量;当它的值小于0时,其绝对值表示等待使用该资源的进程个数。注意,信号量的值仅能由PV操作来改变。

一般来说,信号量S>=0时,S表示可用资源的数量。执行一次P操作意味着请求分配一个单位资源,因此S的值减1;当S<0时,表示已经没有可用资源,请求者必须等待别的进程释放该类资源,它才能运行下去。而执行一个V操作意味着释放一个单位资源,因此S的值加1;若S<0,表示有某些进程正在等待该资源,因此要唤醒一个等待状态的进程,使之运行下去。

1. 创建信号量

通过semget 来创建信号量或获取一个已有的信号量,函数原型:

#include <sys/sem.h>

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

参数:

  • key 信号量的关键字,可以通过ftok() 创建,详细看 进程间通信——消息队列
  • nsems 信号量的数目。如果是创建新的信号量,必须要指定nsems。如果是引用现有的信号量,nsems指定为0。
  • shmflg 有两个选项,IPC_CREAT 表示内核中没有此信号量则创建它。IPC_EXCL 当和IPC_CREAT一起使用时,如果信号量已经存在,则返回错误。

返回值:成功返回标识符,否则返回-1。通过errno和perror函数可以查看错误信息。

通过nsems 可以看出,创建一个信号量,里面可能包含多个信号量值。该信号量就相当于一个集合。该值表明有多少个共享资源单位可供共享应用。下面semctl 也是通过nsems 中的某一个进行操作。

如果nsems 设为1,该信号量又被称为二元信号量(binary semaphore)。

2. 控制信号量

进程间通信——消息队列 和 进程间通信——共享内存,信号量标识符信息被记录在结构体semid_ds中:

struct semid_ds {
    struct ipc_perm  sem_perm;       /* operation permission struct */
    time_t           sem_otime;      /* last semop() time */
    time_t           sem_ctime;      /* last time changed by semctl() */
    unsigned short   sem_nsems;      /* number of semaphores in set */
    ...
}

通过semctl 函数对信号量进行控制或对一些属性进行修改,函数原型:

#include <sys/sem.h>

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

参数:

  • semid 信号的标识符
  • semnum 指定nsems 个信号量中的一个。semnum值在0 和nsems-1 之间,包括0 和 nsems-1。
  • cmd 有10个命令,其中有5中命令是针对一个特定的信号量值的(semnun 指定的)。
  • arg   第4个参数,详细见下面分析。

返回值:若成功返回0(GETALL 等GET 命令详细看下面),若出错,则返回-1。通过errno 和perror 函数可以查看错误信息。

首先来看下第 4 个参数 arg,是否使用取决于所请求的命令,如果使用该参数,则其类型是semun,它是多个命令特定参数的联合,其中包括semid_ds。如下:

union semun {
    int val;                      /* value for cmd SETVAL */
    struct semid_ds *buf;         /* buffer for IPC_STAT & IPC_SET */
    unsigned short int *array;    /* array for GETALL & SETALL */
    struct seminfo *__buf;        /* buffer for IPC_INFO */
}

注意,这个选项参数是一个联合,而非指向联合的指针。

下面来看下cmd 的10 个命令:

  • IPC_STAT  获取semid_ds结构,并存储在由arg.buf 指向的结构中。
  • IPC_SET    按arg.buf 指向的结构中的值,设置与信号量相关的sem_perm.uid、sem_perm.gid 和sem_perm.mode 字段。此命令只能由两种进程执行:一种是其有效用户ID 等于sem_perm.cuid 或sem_perm.uid 的进程;另一种是具有超级用户特权的进程。
  • IPC_RMID  删除信号量。这种删除是立即发生的。删除时仍在使用此信号量的其他进程,在它们下次试图对此信号量进行操作时,将出错返回EIDRM。此命令只能由两种进程执行:一种是其有效用户ID 等于sem_perm.cuid 或sem_perm.uid 的进程;另一种是具有超级用户特权的进程。
  • GETVAL     返回成员semnum 的semval 值
  • SETVAL      设置成员semnum 的semval值,该值由 arg.val 指定。
  • GETPID      返回成员semnum 的sempid 值。
  • GETNCNT  返回成员semnum 的semncnt 值
  • GETZCNT   返回成员semnum 的semzcnt 值
  • GETALL       取出信号量集合中所有信号量的值。这些值存储在 arg.array 指向的数组中。
  • SETALL        将该集合中所有信号量的值设置成arg.array 指向的数组中的值。

对于出GETALL 以外的所有GET 命令,semctl 函数都返回相应值。其他命令,若成功返回0,若出错,返回-1。

3. 信号量PV操作

通过semop 进行 PV 的操作,函数原型:

#include <sys/sem.h>

int semop(int semid, struct sembuf semoparray[], size_t nops);

参数:

  • semid  信号量的标识符
  • semoparray   一个指针,指向一个由sembuf 结构表示的信号量操作数组。
  • nops   规定semoparray 中操作的数量(元素个数)

返回值:若成功,返回0;若失败,返回-1。通过errno和perror函数可以查看错误信息。

sembuf 的数据结构:

struct sembuf {
    unsigned short int sem_num;          /* semaphore number */
    short int sem_op;                    /* semaphore operation */
    short int sem_flg;                   /* operation flag */
}

sem_op值可以是负值、0 或正值。

  • 最易于处理的情况是sem_op 为正值。这对应于进程释放的占用的资源数。sem_op 值会加到信号量上。如果指定了undo 标志,则也从该进程的此信号量调整中减去sem_op。
  • 若sem_op 为负值,则表示要获取由该信号量控制的资源。

如信号量的值大于等于sem_op的绝对值(具有所需的资源),则从信号量值减去sem_op的绝对值。这能保证信号量的结果值大于0。如果指定了undo 标志,则sem_op 的绝对值也加到该进程的此信号量调整值上。

如果信号量值小于sem_op的绝对值(资源不能满足要求),则使用下列条件:

1)若指定了IPC_NOWAIT,则semop 函数返回EAGAIN。

2)若未指定IPC_NOWAIT,则该信号量的semncnt值加1(因为调用进程将进入休眠状态),然后调用进程被挂起直至下列事件之一发生:

    a、此信号量值变成大于等于sem_op的绝对值(即某个进程已经释放了某些资源)。此信号量的semncnt值减1(因为已结束              等待),并且从信号量值中减去sem_op 的绝对值。

          如果指定了undo 标志,则sem_op 的绝对值也加到该进程的此信号量调整值上。

    b、从系统中删除了此信号量。在这种情况下,函数出错返回EIDRM。

    c、进程捕捉到一个信号,从此案好处理程序返回,在这种情况下,此信号量的semncnt值减1(因为调用进程不再等待),并           且函数出错返回EINTR。

  • 若sem_op 为0,这表示调用进程希望等待到该信号值变0.

如果信号量值当前是0,则此函数立即返回。

如果信号量值非0,则使用下列条件:

1)若指定了IPC_NOWAIT,则出错返回EAGAIN。

2)若未指定IPC_NOWAIT,则该信号量的semzcnt值加1(因为调用进程将进入休眠状态),然后调用进程被挂起,直至下面事件发生:

    a、此信号量值变成0。此信号量的semzcnt值减1(因为调用进程已结束等待)。

    b、从系统中删除了此信号量。在这种情况下,函数出错返回EIDRM。

    c、进程捕捉到一个信号,并从信号处理程序返回。在这种情况下,此信号量的semzcnt值减1(因为调用进程不再等待),并           且函数出错返回EINTR。

4. 信号量、记录所和互斥的时间比较

如果在多个进程间共享一个西苑,可以使用3种技术中的一种来协调访问。可以使用映射到两个进程地址空间中的信号量、记录所或者互斥量。

若使用信号量,则先创建一个包含一个成员的信号量集合,然后将该信号量值初始化为1。为了分配资源,以sem_op 为-1 调用semop。为了释放资源,以sem_op 为+1 调用semop。对每个操作都指定SEM_UNDO,以处理在未释放资源条件下进程终止的情况。

若使用记录所,则先创建一个空文件,并且用该文件的第一个字节作为锁字节。为了分配资源,先对该字节获得一个写锁。释放资源时,则对该字节解锁。记录所的性质保证了当一个锁的持有者进程终止时,内核会自动释放该锁。

若使用互斥,需要所有的进程将相同的文件映射到它们的地址空间里,并且使用PTHREAD_PROCESS_SHARED互斥量属性在文件的相同偏移处初始化互斥量。为了分配资源,对互斥量加锁。为了释放锁,解锁互斥量。如果一个进程没有释放互斥量而终止,回复将是非常困难的,除非使用鲁棒互斥量。

下面是在每一种情况下资源都被分配、释放1 000 000次,由3个不同的进程执行的总计(单位秒):

操作用户系统时钟
带undo的信号量0.506.087.55
建议性记录锁0.519.064.38
共享存储中的互斥量0.510.400.25

在Linux 上,记录锁比信号量快,但共享存储中的互斥量的性能比信号量和记录锁都要优越。

如果单一资源加锁,并且不需要信号量的所有花哨功能,那么记录锁将比信号量要好。原因是它使用起来更简单、速度更快,当进程终止时系统会管理遗留下来的锁。

5. 实例

 共享内存 中的例子,test_write 每2秒写一次共享内存,而test_read 是每1 秒读一次共享内存,所以,相同的数据在test_read 会读两次。稍微变化一次,test_write 占用一个信号量,在写共享内存的时候P 信号量(占用、通过),在写完的时候V 信号量(释放)。在test_read 同样使用该信号量,那么虽然1 秒读一次共享内存,但是test_write 不释放,test_read只能睡眠等待。

test_shm.h

#ifndef __TEST_SEMPHORE_INCLUDE__
#define __TEST_SEMPHORE_INCLUDE__

//include this header file for shared memory
#include <sys/sem.h>

#define sem_key "sem_key"
typedef struct sembuf sembuf_t;

int create_sem();
int get_sem();
int remove_sem(int semid);
int p(int semid);
int v(int semid);

#endif //#ifndef __TEST_SEMPHORE_INCLUDE__

test_shm.c

#include <stdio.h>

#include "test_shm.h"

static int create_shm_common(int flags)
{
    printf("create shared memory\n");

    key_t key = ftok(shm_key, 's');
    if (key == -1) {
        perror("ftok error.\n");
        return -1;
    }
    printf("key is 0x%x\n", key);

    int shmid = shmget(key, SHARE_BUF, flags);
    if (shmid == -1) {
        perror("shmget error.\n");
        return -1;
    }
    printf("shmid is %d\n", shmid);

    return shmid;
}

int create_shm()
{
    return create_shm_common(IPC_CREAT /*| IPC_EXCL */| 0644);
}

int get_shm()
{
    return create_shm_common(IPC_CREAT | 0644);
}

int remove_shm(int shmid)
{
    if (shmctl(shmid, IPC_RMID, NULL) == -1) {
        perror("shmctl error.\n");
        return -1;
    }

    return 0;
}

test_sem.h

#ifndef __TEST_SEMPHORE_INCLUDE__
#define __TEST_SEMPHORE_INCLUDE__

//include this header file for shared memory
#include <sys/sem.h>

#define sem_key "sem_key"
typedef struct sembuf sembuf_t;

int create_sem();
int get_sem();
int remove_sem(int semid);
int p(int semid);
int v(int semid);

#endif //#ifndef __TEST_SEMPHORE_INCLUDE__

test_sem.c

#include <stdio.h>
#include <errno.h>

#include "test_sem.h"

int create_sem()
{
    printf("create semphore\n");

    key_t key = ftok(sem_key, 's');
    if (key == -1) {
        perror("ftok error.\n");
        return -1;
    }
    printf("key is 0x%x\n", key);

    int semid = semget(key, 1, IPC_CREAT /*| IPC_EXCL */| 0644);
    if (semid == -1) {
        if (errno == EEXIST) {
            semid = semget(key, 1, 0);
            if (semid != -1)
                return semid;
        }
        perror("semget error.\n");
        return -1;
    }
    printf("semid is %d\n", semid);

    return semid;
}

int get_sem()
{
    printf("get semphore\n");

    key_t key = ftok(sem_key, 's');
    if (key == -1) {
        perror("ftok error.\n");
        return -1;
    }
    printf("key is 0x%x\n", key);

    int semid = semget(key, 0, 0);
    if (semid == -1) {
        perror("semget error.\n");
        return -1;
    }
    printf("semid is %d\n", semid);

    return semid;
}

int remove_sem(int semid)
{
    if (semctl(semid, 1, IPC_RMID) == -1) {
        perror("semctl error.\n");
        return -1;
    }

    return 0;
}

int p(int semid)
{
    sembuf_t sem_p;
    sem_p.sem_num = 0;
    sem_p.sem_op = -1;
    sem_p.sem_flg = SEM_UNDO;

    if (semop(semid, &sem_p, 1) == -1) {
        perror("semop(p) error.\n");
        return -1;
    }

    return 0;
}

int v(int semid)
{
    sembuf_t sem_v;
    sem_v.sem_num = 0;
    sem_v.sem_op = 1;
    sem_v.sem_flg = SEM_UNDO;

    if (semop(semid, &sem_v, 1) == -1) {
        perror("semop(v) error.\n");
        return -1;
    }

    return 0;
}

test_read.c

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

#include "test_shm.h"
#include "test_sem.h"


// server for testing
int main()
{
    printf("main of server for reading shm.\n");

    int shmid = create_shm();

    int semid = create_sem();

    shm_buf_t *shbuf = shmat(shmid, 0, SHM_RDONLY);
    if (shbuf == (void*) -1L) {
        perror("shmat error.\n");
        return -1;
    }
    printf("test_read: Memory attched at %X\n", (int)shbuf);

    int n = 100;

    // add a semphore resource, now there have resource because semphore is +1
    v(semid);
    while (1) {
        p(semid);
        printf("test_read: buf->flag = %d\n", shbuf->flag);
        
        if (shbuf->flag == END || n < 0)
            break;
        sleep(1);
        v(semid);
    }

    remove_sem(semid);

    remove_shm(shmid);

    return 0;
}

test_write.c

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <memory.h>

#include "test_shm.h"
#include "test_sem.h"


// client for testing
int main()
{
    printf("main of server for writing shm.\n");

    int shmid = get_shm();

    int semid = get_sem();

    shm_buf_t *shbuf = shmat(shmid, NULL, 0);
    if (shbuf == (void*) -1L) {
        perror("shmat error.\n");
        return -1;
    }
    printf("test_write: Memory attched at %X\n", (int)shbuf);

    memset(shbuf, 0, SHARE_BUF);
    shm_buf_t *buf = malloc(sizeof(shm_buf_t));

    int n = 20;
    while (n > 0) {
        p(semid);
        if (n == 10) {
            buf->flag = END;
        } else {
            buf->flag = n;
            sleep(2);
        }

        printf("test_write: buf->flag = %d\n", buf->flag);
        memcpy(shbuf, buf, sizeof(shm_buf_t));
        n--;
        v(semid);

        if (n == 9)
            break;
    }

    free(buf);

    return 0;
}

主要注意的是test_read.c 中最开始给一个信号量资源。不然信号量为0,下面的p 函数会一直等待其他进程释放信号量。

代码正常,运行结果就省略了。。。

附Makefile:

ifeq ($(wildcard shm_key),)
$(shell touch shm_key)
endif

ifeq ($(wildcard sem_key),)
$(shell touch sem_key)
endif

target := test_read test_write

all: $(target)

test_read: test_read.o test_shm.o test_sem.o
	gcc -o $@ $^

test_write: test_write.o test_shm.o test_sem.o
	gcc -o $@ $^

%.o: %.c
	gcc -c -o $@ $<

clean:
	@rm -f $(target)
	@rm -f *.o

相关博文:

进程间通信(0)——序 

进程间通信(1)——信号(Signal)

进程间通信(2)——管道(PIPE)

进程间通信(3)——命名管道(FIFO)

进程间通信(4)——消息队列

进程间通信(5)——共享内存

进程间通信(6)——信号量(semaphore​​​​​​)

进程间通信(7)——套接字(socket)

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

私房菜

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值