linux 进程间通信-信号量

更通俗易懂的解释 linux 信号量是什么怎么用?

简介

代码临界区

在多个程序共享资源的情况下, 常常会因为多个程序同时访问一个共享资源而引发一系列问题。为了防止这一问题, 引入了代码临界区的概念。

代码临界区是指操作系统在处理时不可分割的代码。在上述情况下, 即指: 在任一时刻, 只能有一个程序访问临界区并执行代码。

信号量提供了这样一种访问机制。简单来说, 信号量的功能就是用来协调进程对共享资源的访问。

工作原理

程序对信号量 s 的访问都是原子操作, 且只允许它进行 P (占用) 和 V (释放) 信号量两种操作。最常见的信号量取值范围是 0 或 1, 即二进制信号量, 可以取多个正整数的信号量被称为通用信号量。

P(s)和V(s)操作是这样的:

  • P(s): 如果s的值大于零, 就给它减1; 如果它的值为零, 就挂起该进程;
  • V(s): 如果有其他进程因s被占用而被挂起, 就让它恢复运行, 如果没有, 就给它加1.

例子: 如图所示, p1 和 p2 两个进程共享信号量s, p1进程执行了P(s)操作, 它将得到信号量, 并可以进入临界区, 使s减1。而 p2 进程将被阻止进入临界区, 因为当它试图执行P(s)时, s为0, 它会被挂起以等待。当p1 进程离开临界区域并执行V(s)释放信号量, 这时 p2 进程可以恢复执行

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JHPdKpNI-1585228745999)(https://yanyanie.com/2019/05/16/base-linux-sem/sem-section-critique.png)]

经典问题

哲学家进餐问题

桌子上有五个餐盘, 5 个餐叉, 餐叉分布在餐盘的两边。 现在有5 个哲学家, 哲学家可以思考或进食, 但是进食时必须同时使用盘子两边的餐叉。

philosophy(i){
    repeat{
        think();
        eat();
    }
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5uLuPoZZ-1585228746001)(https://yanyanie.com/2019/05/16/base-linux-sem/sem-philo.jpg)]

哲学家之间的进食行为属于互斥行为, 共享的资源是餐叉。当一个哲学家进食时, 由于他必须使用餐盘两边的叉子, 那么与该哲学家相邻的另外两个哲学家因为缺少一个餐叉无法进食。

  • fork[5]: 信号量集[0…4]
  • 对于第 i 个哲学家:
    • 其右手边的餐叉为: i
    • 其左手边的餐叉为: (i+1)%5
  • 初始化信号量集 fork[] 为 1: init(fork[i],1)
philosophy(i){
    repeat{
        think();
        P(fork[i]);
        P(fork[(i+1)%5];
        eat();
        V(fork[i]);
        V(fork[(i+1)%5];
    }
}

算法规定: 每个哲学家先拿起右手边的餐叉, 再拿起左手边的餐叉。这时, 如果 5 个哲学家同时开始进食, 每个哲学家都拿起了右手边的餐叉, 这意味着每个哲学家左手边的餐叉都被另一个哲学家占用, 这样导致没有一个哲学家能吃上饭。这就导致了死锁。

我们进一步思考一下。当所有哲学家同时想要进食时, 会造成死锁, 那么 4 个呢?进一步分析, 当 4 个哲学家同时拿起他们右边的餐叉, 必定有一个哲学家左手边的餐叉是空闲的, 那么这位哲学家就可以进食, 其他三位则需要等待资源, 当这位哲学家吃完放下餐叉, 则等待中的哲学家能够进食。

所以不妨限制同一时间可以发起进食请求的哲学家数量。使同一时间, 桌子边上想要吃饭的哲学家最多只能为 4 人。

  • demande: 信号量, 初始化为 4: init(demande,4)

则上面的算法修正后就避免了死锁问题:

philosophy(i){
    repeat{
        think();
        P(demande);
        P(fork[i]);
        P(fork[(i+1)%5];
        V(demande);
        eat();
        V(fork[i]);
        V(fork[(i+1)%5];
    }
}

作者-读者问题

作者-读者之间的关系, 概括地来说, 即为: 读写互斥, 写写互斥, 读读并发。

算法 1

假设现在有一个文件:

  • 若作者 a 正在写文件, 另一个作者 b 想要修改, 则加入文件等待队列, 直到 a 完成写操作并释放资源
  • 若作者 a 正在写文件, 读者 A 想要修改, 则加入文件等待队列, 直到 a 完成写操作并释放资源
  • 若读者 A 正在读文件, 作者 a 想要修改, 则加入文件等待队列; 需要注意的是, 当所有读者都完成读操作时, 才释放文件资源

我们设定:

  • file: 信号量, 初始为 1: init(file,1)
  • nb_reader: 正在读文件的读者数量, 初始为 0
  • mutex: 信号量, 初始为 1: init(mutex,1) 控制多个读者之间的转换
author(i){
    P(file);
    write();
    V(file);
}
reader(i){
    P(mutex);
    nb_reader = nb_reader + 1;
    if(nb_reader == 1){
        P(file);
    }
    V(mutex);
    
    read()

    P(mutex);
    nb_reader = nb_reader - 1;
    if(nb_reader == 0){
        V(file);
    }
    V(mutex);
}

注意-1: 等待队列只是指请求发生的先后顺序, 并不意味着请求被处理的顺序。实际上, 当等待的请求获得它们所需的资源后, 即可被执行。

比如说, 假设读者 A 正在阅读, 作者 a 发起写请求, 由于需要的 file 资源被读者 A 占用, 所有作者 a 等待; 此时, 若另一个读者 B 发起读请求, 由于 mutex 资源闲置, 所以读者 B 可以进行读操作。

注意-2 假设读者 A 正准备释放文件资源, 同时读者 B (新到来的读请求) 和作者 a 都在等待资源, 由于读者 B 等待的 mutex 资源后于作者 a 等待的 file 资源释放, 所以会先处理作者 a 的请求。

算法 2

在上面的算法中, 如果一个读请求后于一个写请求来到, 但是却有可能先于读请求操作。如果我们想要读写请求按到来的先后顺序进行, 则可以在上述算法的基础上, 添加一个 order 信号量。

  • order: 信号量, 初始为 1: init(order, 0)
author(i){
    P(order);
    P(file);
    write();
    V(file);
    V(order);
}
reader(i){
    P(order);
    P(mutex);
    nb_reader = nb_reader + 1;
    if(nb_reader == 1){
        P(file);
    }
    V(mutex);
    V(order);
    
    read()

    P(mutex);
    nb_reader = nb_reader - 1;
    if(nb_reader == 0){
        V(file);
    }
    V(mutex);
}

这种情况下, 若有读者正在读, 且有作者正在等待, 由于 order 资源被等待的作者占用 (作者等待 file 资源) , 而后面来到的读请求需要等待 ordre 资源,所以后来到的读请求不会先于写请求被处理。

算法 3

在算法 1 中, 假设作者 a 释放资源, 同时读者 A 和作者 b 都在等待资源, 由于读者 A 和作者 b都在等待 file 资源, 因此无法得知谁的请求先被处理。假设为了保证为读者服务的质量, 要求在此情况下, 要让读请求优先于写请求被处理。在算法 1 的基础上进行改动:

我们设定一个新的信号量, 对该情况下的写请求进行阻碍:

  • writebar: 信号量, 初始为 1: init(writebar,1)
author(i){
    P(writebar);
    P(file);
    write();
    V(file);
    V(writebar);
}
reader(i){
    P(mutex);
    nb_reader = nb_reader + 1;
    if(nb_reader == 1){
        P(file);
    }
    V(mutex);
    
    read()

    P(mutex);
    nb_reader = nb_reader - 1;
    if(nb_reader == 0){
        V(file); // V(writebar);
    }
    V(mutex);
}

注意-3: writebar 资源释放可以在 author 中, 也可以在 reader 中, 只要保证后于 file 资源释放就可以了。根据算法 3, 假设作者 a 正准备释放文件资源, 同时读者 A 和作者 b 都在等待资源, 由于读者 A 等待的 file 资源先于作者 b 等待的 writebar 资源释放, 所以会先处理读者 A 的请求。

算法 4

假设现在我们要求读操作也优先于写操作: 即如果一个读请求发生, 则它被加入到等待队列, 直到没有进行或等待的写操作。

  • fifo: 信号量, 初始化为 1: init(fifo, 1)
  • nb_author: 正在执行的作者的数量
  • mutex2: 信号量, 初始化为 1: init(fifo, 1); 作者之间进行转换
author(i){
    P(mutex2);
    nb_author = nb_author + 1;
    if(nb_author == 1){
        P(fifo);
    }
    V(mutex2);

    P(file);
    write();
    V(file);

    P(mutex2);
    nb_author = nb_author - 1;
    if(nb_author == 0){
        V(fifo);
    }
    V(mutex2);
}
reader(i){
    P(fifo);
    P(mutex);
    nb_reader = nb_reader + 1;
    if(nb_reader == 1){
        P(file);
    }
    V(mutex);
    P(fifo);
    
    read()

    P(mutex);
    nb_reader = nb_reader - 1;
    if(nb_reader == 0){
        V(file); // V(writebar)
    }
    V(mutex);
}

若作者 a 正在执行写操作,其他作者可以增加 nb_author, 并等待 file 权限;当所有等待队列中的写操作执行完毕,假设作者 n 正在释放 fifo 资源(即便有一个新的写请求,因为它需要的 mutex2 资源后于 fifo 释放), 正在等待的读者会获取 fifo 权限。

生产者-消费者问题

生产者-消费者问题中既存在互斥关系,又存在同步关系。

  • 互斥:生产者向缓冲区内添加数据,消费者读取缓冲区内的数据。两者对缓冲区的操作互斥,一方修改缓冲区时,另一方不能修改。
  • 同步:缓冲区里面数据不为空,消费者才能读取数据;生产者向缓冲添加数据,缓冲区才不为空。

假设生产者想大小为 BUFFER_SIZE 的缓冲区添加整型数据,消费者按照数据被生产的顺序从缓冲区内进行读取。生产者和消费者同时进行(并行执行)。

  • BUFFER_SIZE:缓冲区大小
  • product :信号量,初始化为 N; 控制生产者生产
  • consuption:信号量,初始化为 0; 控制消费者消费
prod(i){
    P(product);
    produce(tampon[ip])
    ip = (ip + 1) % BUFFER_SIZE
    V(consuption);
}
cons(i){
    P(consuption);
    consume(tampon[ic])
    ic = (ic + 1) % BUFFER_SIZE
    V(product);
}
  • 生产者至少生产一次后,才会释放消费者所需的 consuption 资源
  • ip 和 ic 分别控制生产者和消费者在缓冲区的位置

Linux 信号量接口

Linux提供了一组信号量接口来对信号进行操作, 这些函数都是用来对成组的信号量值进行操作。它们的执行需要声明头文件:

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

semget()

作用: 创建一个新信号量或取得一个已有信号量

原型:

int semget(key_t key, int num_sems, int sem_flags);
  • 返回值:
    • 成功: 信号量集标识符 shm_id
    • 出错: -1, 错误原因存于error中
  • 传入参数:
    • key:
      • 0(IPC_PRIVATE): 建立新信号量集对象
      • 大于0的32位整数: 视参数sem_flgs来确定操作, 通常要求此值来源于ftok返回的IPC键值
    • num_sems: 创建信号量集中信号量的个数, 该参数只在创建信号量集时有效
    • sem_flags:
      • 0: 取信号量集标识符, 若不存在则函数会报错
      • IPC_CREAT: 当sem_flgs&IPC_CREAT为真时, 如果内核中不存在键值与key相等的信号量集, 则新建一个信号量集; 如果存在这样的信号量集, 返回此信号量集的标识符
      • IPC_CREAT|IPC_EXCL: 如果内核中不存在键值与key相等的信号量集, 则新建一个消息队列; 如果存在这样的信号量集则报错
      • sem_flgs参数为模式标志参数, 使用时需要与IPC对象存取权限 (如0600) 进行|运算来确定信号量集的存取权限
  • 错误代码:
    • EACCESS: 没有权限
    • EEXIST: 信号量集已经存在, 无法创建
    • EIDRM: 信号量集已经删除
    • ENOENT: 信号量集不存在, 同时semflg没有设置IPC_CREAT标志
    • ENOMEM: 没有足够的内存创建新的信号量集
    • ENOSPC: 超出限制

semop()

  • 作用: 对信号量集标识符为semid中的一个或多个信号量进行P操作或V操作

  • 原型:

    int semop(int sem_id, struct sembuf *sem_opa, size_t num_sem_ops);
    
  • 返回值:

    • 成功: 信号量集标识符
    • 出错: -1, 错误原因存于error中
  • 传入参数:

    • sem_id: 信号量集标识符

    • num_sem_ops: 进行操作信号量的个数, 即 sem_opa 结构变量的个数, 需大于或等于1。最常见设置此值等于1, 只完成对一个信号量的操作

    • sem_opa 指向进行操作的信号量集结构体数组的首地址

      • semnum: 信号量集合中的信号量编号, 0代表第1个信号量

      • val
        

        :

        • 若val>0进行V操作信号量值加val, 表示进程释放控制的资源
        • 若val<0进行P操作信号量值减val
          • 若(semval-val)<0 (semval为该信号量值) , 则调用进程阻塞, 直到资源可用;
          • 若设置 IPC_NOWAIT 不会睡眠, 进程直接返回 EAGAIN 错误
      • flag
        

        :

        • 0 设置信号量的默认操作
        • IPC_NOWAIT 设置信号量操作不等待
        • SEM_UNDO 选项会让内核记录一个与调用进程相关的UNDO记录, 如果该进程崩溃, 则根据这个进程的UNDO记录自动恢复相应信号量的计数值
      struct sembuf {
          short semnum;
          short val;
          short flag;
      };
      
  • 错误代码:

    • E2BIG: 一次对信号量个数的操作超过了系统限制
    • EACCESS: 权限不够
    • EAGAIN: 使用了IPC_NOWAIT, 但操作不能继续进行
    • EFAULT: sops指向的地址无效
    • EIDRM: 信号量集已经删除
    • EINTR: 当睡眠时接收到其他信号
    • EINVAL: 信号量集不存在,或者semid无效
    • ENOMEM: 使用了SEM_UNDO, 但无足够的内存创建所需的数据结构
    • ERANGE: 信号量值超出范围

semctl()

作用: 直接控制信号量信息

原型:

int semctl(int sem_id, int sem_num, int command, union semun arg);
  • 返回值:

    • 成功: 大于或等于0
    • 出错: -1, 错误原因存于error中
  • 传入参数:

    • sem_id: 信号量集标识符

    • sem_num: 信号量集数组上的下标, 表示某一个信号量

    • command:

      • IPC_STAT: 从信号量集上检索semid_ds结构, 并存到semun联合体参数的成员buf的地址中
      • IPC_SET: 设置一个信号量集合的semid_ds结构中ipc_perm域的值, 并从semun的buf中取出值
      • IPC_RMID: 从内核中删除信号量集合
      • GETALL: 从信号量集合中获得所有信号量的值, 并把其整数值存到semun联合体成员的一个指针数组中
      • GETNCNT: 返回当前等待资源的进程个数
      • GETPID: 返回最后一个执行系统调用semop()进程的PID
      • GETVAL: 返回信号量集合内单个信号量的值
      • GETZCNT: 返回当前等待100%资源利用的进程个数
      • SETALL: 与GETALL正好相反
      • SETVAL: 用联合体中val成员的值设置信号量集合中单个信号量的值
    • arg:

      • val: SETVAL用的值

      • semid_ds: IPC_STAT、IPC_SET用的semid_ds结构

      • array: SETALL、GETALL用的数组值

      • seminfo: 为控制IPC_INFO提供的缓存

        union semun {
            short val;          
                    struct semid_ds* buf;
                    unsigned short* array; 
                    struct seminfo *buf;
                }arg;
        
  • 错误代码:

    • EACCESS: 权限不够
    • EFAULT: arg指向的地址无效
    • EIDRM: 信号量集已经删除
    • EINVAL: 信号量集不存在, 或者semid无效
    • EPERM: 进程有效用户没有cmd的权限
    • ERANGE: 信号量值超出范围

应用

以下代码用 C 语言实现,并在 Linux 环境下运行。

Linux 信号量实现

初始化一个数量为 5 的信号量集,定义信号量集初始化、删除、更改信号量集中某个信号量的值,以及P(s)和 V(s)操作:

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

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

#include <time.h>

#define N_SEM 5 // 信号量集中信号量的数目

int init_semaphore(void); // 初始化信号量集
int detruire_semaphore(void); // 删除信号量集
int val_sem(int, int); // 更改信号量集中某个信号量的值
int P(int);
int V(int);
// semaph.c
# include "semaph.h"

static int semid = -1; // 信号量集标识符

// struct sembuf {short semnum; short val; short flag;};
static struct sembuf 
    sops_p = {-1, -1, 0},
    sops_v = {-1, 1, 0};

union semun
{
	int val;
};

int init_semaphore(){
    int i;
	union semun arg0;

	if(semid != -1)
	{
		fprintf(stderr, "init_semaphore: Semaphores deja initialises\n");
		return -1;
	}

	if( (semid = semget(IPC_PRIVATE, N_SEM, 0600)) == -1)
	{
		fprintf(stderr, "init_semaphore/semget: Echec %d\n", errno);
		return -2;
	}

	arg0.val = 0;

	for(i = 0; i < N_SEM; i++)
		if( (semctl(semid, i, SETVAL, arg0)) == -1)
		{
            fprintf(stderr, "init_semaphore/semctl: Echec\n");
			return -2;
		}

	return 0;
}

int detruire_semaphore(){
    int i;
    int retour;

	if(semid == -1)
	{
		fprintf(stderr, "detruire_semaphore: semaphore inexistant\n");
		return -1;
	}

    retour = semctl(semid, 0, IPC_RMID, 0);
	semid = -1;

	return retour;
}

int val_sem(int sem, int val){
	union semun arg0;

	if(semid == -1)
	{
		fprintf(stderr, "val_sem: Semaphore inexistant\n");
		return -1;
	}

	if(sem < 0 || sem >=N_SEM){
		fprintf(stderr, "val_sem: Num de semaphore inexistant\n");
		return -2;
	}

	arg0.val = val;

	return semctl(semid, sem, SETVAL, arg0);
}

int P(int sem){
    if(semid == -1)
	{
		fprintf(stderr, "P: Sémaphore inexistant\n");
		return -1;
	}

	if(sem < 0 || sem >=N_SEM)
	{
		fprintf(stderr, "P: Numéro de sémaphore inexistant\n");
		return -2;
	}
    
	sops_p.sem_num = sem;

	return semop(semid, &sops_p, 1);

}

int V(int sem){
    if(semid == -1)
	{
		fprintf(stderr, "V: Semaphore inexistant\n");
		return -1;
	}

	if(sem < 0 || sem >=N_SEM)
	{
		fprintf(stderr, "V: Num de semaphore inexistant\n");
		return -2;
	}

	sops_v.sem_num = sem;

    return semop(semid, &sops_v, 1);
}

生产者-消费者

该问题使用上述创建的semaph.hsemaph.c

  • 创建一个父子进程,子进程作为生产者,向一个大小为 5 的缓冲区内添加 int 数据。父进程作为消费者,按照数据添加顺序读取缓冲区内数据
  • 当缓冲区数据为空时,消费者无法进行消费;当缓冲区数据为满时,生产者无法添加数据
  • 该问题采用上面提到的生产者-消费者问题算法
共享内存创建缓冲区
// sharemem.h
#include <sys/time.h>
#include <sys/ipc.h>
#include <sys/shm.h>

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

#define SHMSZ 5 * sizeof(int)

int createshm(void);
int* bindshm(int);
// sharemem.c
#include "sharemem.h"

int createshm(void){
    int shmid;
    key_t key; 
    key = 2017;
    shmid = shmget(key, SHMSZ, IPC_CREAT|0666);
    if (shmid < 0){
        perror("sharemem.c/createshm: shmget\n");
        exit(1);
    }
    printf("Creer Shmid = %d\n", shmid);
    return shmid;
}

int* bindshm(int shmid){
    int *shm;
    if((shm=shmat(shmid,NULL,0)) == (int*)-1){
        perror("sharemem.c/bindshm: Echec\n");
        exit(1);
    }
    return shm;
}
主程序
// prod-conso.c
#include "semaph.h"
#include "sharemem.h"
#include <wait.h>

#define BUFFER_SIZE 5

int main(int argc, char *argv[])
{   
    int shmid = createshm();
    int *tampon = bindshm(shmid);

    init_semaphore();
    val_sem(2,BUFFER_SIZE);
    val_sem(3,0);

    int i = 0 ;
    switch (fork()) {
        case -1:
            perror("prod-conso/fork: Echec\n");
            break;
        case 0:
           for(i;i<10;i++){ 
                P(2);
                tampon[i%BUFFER_SIZE] = i + 1;
                printf("produit: %d \n",tampon[i%BUFFER_SIZE]);
                V(3);
                usleep(((rand()%(100-20))+20)*1000); 
            }
            break;
        default:
            for(i;i<10;i++){
                usleep(((rand()%(100-20))+20)*1000);
                P(3);
                printf("consomme: %d \n",tampon[i%BUFFER_SIZE]);
                V(2);
            }
    }
    wait(NULL);
    detruire_semaphore();
    shmdt(tampon);
    shmctl(shmid,IPC_RMID,0); 
    return 0;
}
程序执行和检测

为了运行主程序,我们需要使用之前创建的两个库文件: semaph.csharemem.c

使用ar命令将两个库文件集合成单一的备存文件

// 将程序预处理,编译,和汇编, 生成.o的obj文件
gcc -c semaph.c 
gcc -c sharemem.c

// 合成单一的备存文件
ar rvs libsempv.a semaph.o sharemem.o 

// 查看 libsempv.a 
nm -s libsempv.a 

// 生成可执行文件
gcc -o prod-conso prod-conso.c -L. libsempv.a

//执行文件
./prod-conso

文件执行结果:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eVVpdhgh-1585228746003)(https://yanyanie.com/2019/05/16/base-linux-sem/prod-conso.png)]

);
P(3);
printf(“consomme: %d \n”,tampon[i%BUFFER_SIZE]);
V(2);
}
}
wait(NULL);
detruire_semaphore();
shmdt(tampon);
shmctl(shmid,IPC_RMID,0);
return 0;
}


#### 程序执行和检测

为了运行主程序,我们需要使用之前创建的两个库文件: `semaph.c` 和 `sharemem.c`

使用ar命令将两个库文件集合成单一的备存文件

// 将程序预处理,编译,和汇编, 生成.o的obj文件
gcc -c semaph.c
gcc -c sharemem.c

// 合成单一的备存文件
ar rvs libsempv.a semaph.o sharemem.o

// 查看 libsempv.a
nm -s libsempv.a

// 生成可执行文件
gcc -o prod-conso prod-conso.c -L. libsempv.a

//执行文件
./prod-conso


文件执行结果:
[[外链图片转存中...(img-eVVpdhgh-1585228746003)]](https://yanyanie.com/2019/05/16/base-linux-sem/prod-conso.png)

-------------E N D-------------
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值