Linux 进程间通信 信号量

13 篇文章 0 订阅
5 篇文章 0 订阅

Linxu下的信号量
  信号量用于协调进程间的运行步调,即进程同步。经典的生产者消费者问题,就是典型的应用场景之一。另外,封装的二元信号量可以以哦那uong用于保护进程间共享的临界资源,类似于在多线程程序中用互斥量保护全局临界区。
实际上,信号量在线程互斥量之前就已经出现了,因为早在多线程出现之前,进程间就已经存在同步运行步调的需求了。信号量通常配合共享内存使用。
  信号量的工作逻辑相对比较简单,有增加,减少和检查三种操作。
  减少操作时,如果当前信号量过少,即减少指定值后小于0,执行减操作的进程将被挂起,直到有进程执行加操作且所加的值足够减操作时,挂起的进程才会被唤醒。

  检查操作用来检查当前信号量的值是否为0,不是0,该进程被挂起,直到该信号量被其它进程修改为0时再被唤醒。

  另外,进程可以同时操作多个信号量。这种情况下,对组内所有信号量的原子操作都是原子性的,也就是说。要么同时完成组内所有信号量的指定操作,要么挂起进程,不指行组内任何一个信号量的操作。

  信号量的创建和初始化分两个步骤进行,当多个平行进程有可能同时运行时,需要特别注意可能出现的竞争条件。

  1. 创建和操作
    创建和操作信号量的函数为:
int semget(key_t key, int nsems, int semflag);
int semctl(int semid, int semnum, int cmd, ...);
int semop(int semid, struct sembuf * sops, unsigned int nsops);

semget 用于创建一组信号量,nsems 指定创建的信号量的数量,key 和 semflag 分别是信号量的整数标识和控制标志,取值和意义与消息队列中的 key 和 flag 参数一样;
semctl() 用于控制信号量,包括初始化、删除、状态查询等;
semop() 用于对信号量执行一组操作,sops 参数提供了所需执行的操作组,是指向 sembuf 结构数组的一个指针,nsops 指定了该数组的元素个数,sembuf 结构定义如下:

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

其中,sem_num 指示要操作的信号量在组内的位置坐标;sem_op 指定要执行的操作,大于 0 标识执行增加操作,小于 0 标识则执行减少操作,等于 0 标识便检查信号量是否为 0。

sem_flag 有两个取值,一个为 IPC_NOWAIT,表示当该信号量不能完成操作时,semop 会立即返回 EAGAIN 错误,而不是挂起进程,且同组内所有操作都不执行;另一个为 SEM_UNDO,表示需要内核记录该信号量操作的效果,当进程终止(不管有意退出还是意外终止)时,要求内核撤销该操作,以防止进程意外终止时其他进程因正在等待一个信号量而被永久阻塞。

  1. 内核结构
    信号量在内核中的结构如下:
    在这里插入图片描述
    在信号量结构中,sem_array 下的 sem_base 字段指向一个 sem 结构的数组,数组中包含了指定数量的信号量值(semval)以及最近一次访问该信号量的进程 ID(sempid)。

当前未完成的信号量操作均存放在 sem_pending 指向的 sem_queue 结构的双向队列中,下面还有一个辅助字段 sem_pending_last,用来指向该队列的最后一个元素,以方便新加入的操作被追加到队列的队尾。

如果信号量操作时指定了 IPC_UNDO 标志,该操作会被记录到 sem_undo 结构的链表中,链表的每个元素记录了某个进程对信号量操作的累加结果,用于在进程终止时撤销对特定信号量的操作。当前挂起的操作如果指定了 IPC_UNDO 标志,在其结构中也会有 undo 字段指向相应的 sem_undo 结构在队列中的位置。

在 Linux 系统中,信号量的各种限制定义在 /proc/sys/kernel/sem 文件中,它包含四个用空格分隔的数字,这四个数字代表的意义请看如下说明。

SEMMSL:一组信号量所能包含的最大数量的信号量,常见默认值是 250。
SEMMNS:系统所能包含的最大数量的信号量,常见默认值是 32000。
SEMOPM:每个 semop 系统调用能执行的操作的最大数量,常见默认值是 32。
SEMMNI:能创建的信号量组的数量,常见默认值是 128。

//以下参考自:
https://blog.csdn.net/a1414345/article/details/64513946

进程间通信概述

1. 进程通信机制

  一般情况下,系统中运行着大量的进程,而每个进程之间并不是相互独立的,有些进程之间经常需要互相传递消息。但是每个进程在系统中都有自己的地址空间,操作系统通过页表和实际物理内存所关联,不允许其他进程随意进入。因此,就必须有一种机制既能保证进程之间的通信,又能保证系统的安全,即进程间通信机制——I P C (Inter_Process Communication)。
  Linux中的内存空间分为系统空间和用户空间。在系统空间中,由于各个线程的地址空间都是共享的,即一个线程能够随意访问kernel中的任意地址,所以无需进程通信机制的保护。而在用户空间中,每个进程都有自己的地址空间,一个进程为了与其他进程通信,必须陷入到有足够权限访问其他进程空间的kernel中,从而与其他进程进行通信。在Linux中支持System V 进程通信的手段有三种:消息队列(Message queue)、信号量(Semaphore)、共享内存(Shared memory)。

2. 进程通信对象标示符和键

  在kernel中,对每一类I P C 对象,都由一个非负整数来索引。为了识别并唯一标识各个进程通信的对象,需要一个标识符(即IPC标示符)来标识各个通信对象。而为了获取一个独一无二的通信对象,必须使用键(可使用ftok( )函数生成,返回值key)。这里的键是用来定位I P C 对象的标识符的。

背景知识

1. 原子操作(atomic operation)

原子操作意为不可被中断的一个或一系列操作,也可以理解为就是一件事情要么做了,要么没做。而原子操作的实现,一般是依靠硬件来实现的。

2. 同步与互斥

同步:在访问资源的时候,以某种特定顺序的方式去访问资源
互斥:一个资源每次只能被一个进程所访问。

3. 临界资源

不同进程能够看到的一份公共的资源(如:打印机,磁带机等),且一次仅允许一个进程使用的资源称为临界资源。

4. 临界区

临界区是一段代码,在这段代码中进程将访问临界资源(例如:公用的设备或是存储器),当有进程进入临界区时,其他进程必须等待,有一些同步的机制必须在临界区段的进入点和离开点实现,确保这些共用资源被互斥所获得。

5. 相关命令

ipcs -s 显示已存在的信号量
ipcrm -s 删除指定信号量
注意:有时候因为权限问题需要在root下查看与删除。

什么是信号量(Semaphore)?
信号量(Semaphore)可以被看做是一种具有原子操作的计数器,它控制多个进程对共享资源的访问,通常描述临界资源当中,临界资源的数目,常常被当做锁(lock)来使用,防止一个进程访问另外一个进程正在使用的资源。

信号量本身不具有数据交换的功能,而是控制其他资源来实现进程间通信,在此过程中负责数据操作操作的互斥、同步等功能。
简言之:信号量的主要目的是为了保护临界资源。

1. 为什么要使用信号量
为了防止出现因多个进程同时访问一个共享资源而引发的问题,我们需要一种方法,可以通过生成并使用令牌来授权,在任一时刻只能有一个执行流访问代码的临界区域。而信号量就可以提供这样的一种访问机制,让一个临界区同一时刻只有一个执行流在访问它。

2. 信号量的工作原理
测试控制该资源的信号量。
若此信号量的值为正,则进程可以使用该资源。进程将信号量值减1,,表示一个资源被使用。
若此信号量的值为0,则进程进入休眠状态,直至信号量值大于0,进程被唤醒,从新进入第1步。
当进程不再使用由一个信号控制的共享资源时,该信号量值增1,如果有进程正在休眠等待该信号量,则会被唤醒。
为了正确地实现信号量,信号量的操作应是原子操作,所以信号量通常是在内核中实现的。
3. Linux的信号量机制
在System V中信号量并非是单个非负值,而必须将信号量定义为含有一个或多个信号量值的集合。当创建一个信号量时,要指定该集合中信号量值的数量。

创建信号量(semget)和对信号量赋初值(semctl)分开进行,这是一个弱点,因为不能原子地创建一个信号量集合,并且对该集合中各个信号量赋初值。

即使没有进程在使用I P C资源,它们仍然是存在的,要时刻防止资源被锁定,避免程序在异常情况下结束时没有解锁资源,可以使用关键字(SEM_UNDO )在退出时恢复信号量值为初始值。

相关函数
1、ftok函数

#include <sys/ipc.h>
#include <sys/types.h>
key_t ftok(const char* path, int id);

ftok 函数把一个已存在的路径名和一个整数标识转换成一个key_t值,即IPC关键字

path 参数就是你指定的文件名(已经存在的文件名),一般使用当前目录。当产生键时,只使用id参数的低8位。

id 是子序号, 只使用8bit (1-255)

返回值:若成功返回键值,若出错返回(key_t)-1

在一般的UNIX实现中,是将文件的索引节点号取出(inode),前面加上子序号得到key_t的返回值

2、semget函数

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semget( key_t key, int nsems, int semflg);

用来创建一个信号集,或者获取已存在的信号集。

key: 所创建或打开信号量集的键值(ftok成果执行的返回值)。

nsems:创建的信号量集中的信号量个数,该参数只在创建信号量时有效。

semflg :调用函数的操作类型,也可用于设置信号量集的访问权限,通过or运算使用。

IPC_CREAT | IPC _EXCL | 0666 :一般用于创建,可保证返回一个新的ID,同时制定权限为666
IPC_CREAT : 用于获取一个已经存在的ID
返回值:成功返回信号量集的标识符,失败返回-1,errno被设置成以下的某个值:

EACESS : 没有访问该信号量集的权限。
EEXIST:信号量集已经存在,无法创建。
EINVAL:参数nsems的值小于0,或者大于该信号量集的限制,或者是该key关联的信号量以存在,并且nsems的值大于该信号量集的信号量数。
ENOENT:信号量集不存在,同时没有使用,IPC_CREAT。
ENOMEM:没有足够的内存创建新的信号量集。

3、semctl函数

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semctl(int semid, int semun, int cmd, ...);

用来初始化信号集,或者删除信号集。

semid:信号量集I P C 标识符。

semun:操作信号在信号集中的编号,第一个信号的号是0.

cmd:在semid指定的信号量集合上执行此命令。

第四个参数是可选的,如果使用该参数,则其类型是semun,它是多个特定命令参数的联合(union):


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

第三个参数cmd常用命令:

IPC_SEAT:对此集合取semid_ds 结构,并存放在由arg.buf指向的结构中。
IPC_RMID:从系统中删除该信号量集合。
SETVAL:设置信号量集中的一个单独的信号量的值,此时需要传入第四个参数。
返回值:成功返回一个正数,失败返回-1。

4、 semop函数

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

功能:操作一个或一组信号。也可以叫PV操作

semid:信号集的识别码,可以通过semget获取。

sops:是一个指针,指向一个信号量操作数组。信号量操作由结构体sembuf 结构表示如下:

struct sembuf
{
    unsigned short sem_num;  // 在信号集中的编码 0 , 1, ... nsems-1
    short  sem_op;     //操作  负值或正值
    short  sem_flg;    // IPC_NOWAIT, SEM_UNDO
};

sembuf结构体参数说明:

sem_num:操作信号在信号集中的编号,第一个信号的编号是0,最后一个信号的编号是nsems-1。

sem_op:操作信号量

若sem_op 为负(P操作), 其绝对值又大于信号的现有值,操作将会阻塞,直到信号值大于或等于sem_op的绝对值。通常用于获取资源的使用权。

若sem_op 为正(V操作), 该值会加到现有的信号内值上。通常用于释放所控制资源的使用权。

sem_op的值为0:如果没有设置IPC_NOWAIT,则调用该操作的进程或线程将暂时睡眠,直到信号量的值为0;否则进程或线程会返回错误EAGAIN。
sem_flg: 信号操作标识,有如下两种选择:

IPC_NOWAIT:对信号的操作不能满足时,semop()不会阻塞,并立即返回,同时设定错误信息。

SEM_UNDO:程序结束时(正常退出或异常终止),保证信号值会被重设为semop()调用前的值。避免程序在异常情况下结束时未解锁锁定的资源,早成资源被永远锁定。造成死锁。

nsops:信号操作结构的数量,恒大于或等于1.

返回值:成功执行时,都会回0,失败返回-1,并设置errno错误信息。

代码演示
(1)、目的阐述:使用P操作和V操作控制临界区,实现两个进程(父进程与子进程)打印不同的文字,在一个进程进入临界区时,另一进程等待。下面使用的测试是,让子进程在它的临界区打印出(你好:),让父进程打印出(在吗?)。

(2)、在没有使用信号量控制时,打印出来的汉字顺序不是通顺的话。在加入PV操作后,可以保证一个进程打印完,另外一个进程继续打印剩下的汉字,而不会互相交叉。

注:

  1. 对于第一次进入临界区时父进程先进入还子进程先进入,与操作系统的进程调度算法有关。
  2. 没有在程序中设置跳出循环条件,可以ctrl+c 结束后,用命令去删除信号量集。

代码:

mysem_h

#ifndef _MYSEM_H_
#define _MYSEM_H_

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

#define PATHNAME "."  // ftok 中生成key值 . 表示当前路径
#define PROJ_ID  56  // ftok 中配合PATHNAME 生成唯一key值


int create_sems(int nums);  // 创建含有nums个信号量的集合
int get_sems();     // 获取信号量

// 初始化semid对应的信号量集中编号为which的信号量值为value
int init_sems(int semid , int which, int value);

int destroy_sems(int semid); // 释放该信号量集


int P(int semid, int which);    // 表示分配 信号量值-1
int V(int semid, int which);    // 表示释放 信号量值+1

#endif /* _MYSEM_H_ */

mysem_c

#include "mysem.h"

// 创建信号量和获取信号量公用函数
static int comm_sem ( int nums , int semflag)
{
    // 获取key
    key_t key = ftok(PATHNAME, PROJ_ID);
    if(key < 0)
    {
        perror("ftok");
        return -1;
    }

    int semid = semget(key,nums, semflag );
    if( semid < 0)
    {
        perror("semget");
        return -1;
    }
    return semid;
}

int create_sems(int nums)  // 创建含有nums个信号量的集合
{
    return comm_sem(nums, IPC_CREAT|IPC_EXCL|0666);
}


int get_sems()     // 获取信号量
{
    return comm_sem(0, IPC_CREAT);
}

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

// 初始化semid对应的信号量集中编号为which的信号量值为value
int init_sems(int semid , int which, int value)
{
    union semun _semun;
    _semun.val = value;
    int ret = semctl(semid, which, SETVAL,_semun);
    if(ret < 0)
    {
        perror("inin_sem");
        return -1;
    }
    return 0;
}

int destroy_sems(int semid) // 释放该信号量集
{
    int ret = semctl(semid, 0, IPC_RMID, NULL);
    if(ret < 0)
    {
        perror("rm_sem");
        return -1;
    }
    return 0;
}

static int comm_sem_op(int semid, int which, int op)
{
    struct sembuf _sembuf;
    _sembuf.sem_num = which;
    _sembuf.sem_op = op;
    _sembuf.sem_flg = 0; //  IPC_NOWAIT  SEM_UNDO
    return semop(semid, &_sembuf, 1);
}


int P(int semid, int which)    // 表示通过 信号量值-1
{
    return comm_sem_op(semid, which , -1);
}
int V(int semid, int which)    // 表示释放 信号量值+1
{
    return comm_sem_op(semid, which, 1);
}

test_mysem_c

// 加入信号量操作后的程序
#include "mysem.h"
#include <stdio.h>
#include <unistd.h>

int main()
{
    int semid = create_sems(10); // 创建一个包含10个信号量的信号集
    init_sems(semid, 0, 1);  // 初始化编号为 0 的信号量值为1

    pid_t id = fork(); // 创建子进程
    if( id < 0)
    {
        perror("fork");
        return -1;
    }
    else if (0 == id)
    {// child 
        int sem_id = get_sems();
        while(1)
        {
            P(sem_id, 0); // 对该信号量集中的0号信号  做P操作
            printf("你");
            fflush(stdout);
            sleep(1);
            printf("好");
            printf(":");
            fflush(stdout);
            sleep(1);
            V(sem_id, 0);
        }
    }
    else
    {// father
        while(1)
        {
            P(semid,0);
            printf("在");
            sleep(1);
            printf("吗");
            printf("?");
            fflush(stdout);
            V(semid, 0);
        }
        wait(NULL);
    }

    destroy_sems(semid);
    return 0;
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值