Linux中的信号量

Linux中的信号量

信号量的概念

信号量本质上是一个计数器,用于协调多个进程(包括但不限于父子进程)或多个线程对共享数据对象的读/写。它不以传送数据为目的,一般会有两种作用:

  1. 用来保护共享资源(共享内存、消息队列、socket连接池、数据库连接池等),保证共享资源在一个时刻只有一个进程独享,类似于互斥锁的操作
  2. 用来实现生产者-消费者模型,其中生产者每生产一个对象对信号量+1,消费者每消费一个对象对信号量-1,如果没有可以被消费的对象则阻塞等待

信号量是一个特殊的变量,只允许进程(线程)对它进行等待信号和发送信号操作。最简单的信号量是取值0和1的二元信号量,这是信号量最常见的形式。

Linux中的信号量使用

在Linux中,信号量有着两种实现:「传统的Systm V信号量」和「POSIX信号量」

对于这两种信号量在系统所提供的函数中很容易进行区分:

  • 所有的Systm V信号量的函数名中没有下划线,例如semget()而不是sem_get()
  • 相对应的,所有的POSIX信号量函数都有下划线
  • 另外,对于POSIX信号量,可以有命名的信号量,例如信号量有一个文件关联他们
    • 比如在下面对照表中的POSIX最后三个函数就是用来关闭、打开、删除这样一个命名的信号量,而sem_destroy()sem_init()仅仅供非命名信号量使用
Systm V POSIX
semctl() sem_getvalue()
semget() sem_post()
semop() sem_timedwait()
sem_trywait()
sem_wait()
sem_destroy()
sem_init()
sem_close()
sem_open()
sem_unlink()

引用一下网上对于这两者使用时的结论:

POSIX信号量来源于POSIX技术规范的实时扩展方案(POSIX Realtime Extension),常用于线程(但其有名信号量也可用于进程);system v信号量,常用于进程的同步。

这两者非常相近,但它们使用的函数调用各不相同。前一种的头文件为semaphore.h,函数调用为sem_init()sem_wait()sem_post()sem_destory()等等。后一种头文件为sys/sem.h,函数调用为semctl()semget()semop()等函数

System V信号量的使用

头文件

1
2
3
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>

semget函数

  • 作用:得到一个信号量集标识符或创建一个信号量集对象并返回信号量集标识符

  • 函数原型:

    1
    
    int semget(key_t key, int nsems, int semflg);
    
  • 参数:

    • key是信号量的键值,(类型定义:typedef unsigned int key_t),是信号量在系统中的编号,不同信号量的编号不能相同,这一点由程序员保证。key用十六进制表示比较好,便于查看和管理
    • nsems是创建信号量集中信号量的个数,该参数只在创建信号量集时有效,其他情况填1即可
    • semflag是一组标志,如果希望信号量不存在时创建一个新的信号量,可以和值IPC_CREAT做按位或操作。如果没有设置IPC_CREAT标志并且信号量不存在,就会返错误(errno的值为2,No such file or directory)。
  • 返回值:

    • 成功:返回信号量集的标识

    • 失败:返回-1,并设置errno

      错误代码:

      • EACCESS:没有权限
      • EEXIST:信号量集已经存在,无法创建
      • EIDRM:信号量集已经删除
      • ENOENT:信号量集不存在,同时semflg没有设置IPC_CREAT标志
      • ENOMEM:没有足够的内存创建新的信号量集
      • ENOSPC:超出限制
  • 如果semget创建了一个新的信号量集对象,则semid_ds结构成员变量的值设置如下:

    • sem_otime设置为0
    • sem_ctime设置为当前时间
    • msg_qbytes设成系统的限制值
    • sem_nsems设置为nsems参数的数值
    • semflg的读写权限写入sem_perm.mode中
    • sem_perm结构的uid和cuid成员被设置成当前进程的有效用户ID,gid和cuid成员被设置成当前进程的有效组ID
  • 示例代码:

    1
    2
    
    // 获取键值为0x5000的信号量,如果该信号量不存在,就创建它,代码如下:
    int semid=semget(0x5000,1,0640|IPC_CREAT);
    
    1
    2
    
    // 获取键值为0x5000的信号量,如果该信号量不存在,返回-1,errno的值被设置为2,代码如下:
    int semid= semget(0x5000,1,0640);
    

semctl函数

  • 作用:控制信号量(常用于设置信号量的初始值和销毁信号量)

  • 函数原型:

    1
    
    int semctl(int semid, int sem_num, int command, ...);
    
  • 参数:

    • semid是由semget函数返回的信号量标识

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

    • 参数cmd是对信号量操作的命令种类,常用的有以下两个:

      • IPC_RMID:销毁信号量,不需要第四个参数;

      • SETVAL:初始化信号量的值(信号量成功创建后,需要设置初始值),这个值由第四个参数决定。第四参数是一个自定义的联合体,联合体结构如下:

        1
        2
        3
        4
        5
        6
        7
        
        // 用于信号灯操作的共同体。
        union semun
        {
          int val;
          struct semid_ds *buf;
          unsigned short *arry;
        };
        
  • 返回值:

    • 成功:返回值根据操作的不同返回不同的值,具体查看手册
    • 失败:返回-1
  • 示例代码:

    1
    2
    
    // 销毁信号量
    semctl(semid,0,IPC_RMID);
    
    1
    2
    3
    4
    
    // 初始化信号量的值为1,信号量可用
    union semun sem_union;
    sem_union.val = 1;
    semctl(semid,0,SETVAL,sem_union);
    

semop函数

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

    • V操作:把信号量的值+1,这个过程也称之为释放锁
    • P操作:等待信号量的值大于1,如果等待成功,立即把信号量的值-1,这个过程也称之为等待锁
  • 函数原型:

    1
    
    int semop(int semid, struct sembuf *sops, unsigned nsops);
    
  • 参数:

    • semid:由semget函数返回的信号量标识

    • nsops:操作信号量的个数,即sops结构变量的个数,设置它的为1(只对一个信号量的操作)

    • sops是一个指向进行操作的信号量集结构体数组的首地址的指针,此结构结构如下:

       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      
      struct sembuf
      {
        short sem_num;   
        short sem_op;    // 。
        short sem_flg;   
      };
      
      struct sembuf
      {
        unsigned short int sem_num;	// 信号量集的个数,单个信号量设置为0
        short int sem_op;		// 大于零则进行V操作+对应值,小于零则进行P操作-对应值,如果剩余量不够减则会阻塞等待;
        short int sem_flg;	// 把此标志设置为SEM_UNDO,操作系统将跟踪这个信号量。
                         // 如果当前进程退出时没有释放信号量,操作系统将释放信号量,避免资源被死锁。
      };
      
  • 返回值:

    • 成功:返回信号量集的标识符
    • 失败:返回-1,并设置errno 错误代码:
      • E2BIG:一次对信号量个数的操作超过了系统限制
      • EACCESS:权限不够
      • EAGAIN:使用了IPC_NOWAIT,但操作不能继续进行
      • EFAULT:sops指向的地址无效
      • EIDRM:信号量集已经删除
      • EINTR:当睡眠时接收到其他信号
      • EINVAL:信号量集不存在,或者semid无效
      • ENOMEM:使用了SEM_UNDO,但无足够的内存创建所需的数据结构
      • ERANGE:信号量值超出范围
  • 示例代码:

    1
    2
    3
    4
    5
    6
    
    // 等待信号量的值大于0,如果等待成功,立即把信号量的值减1;
    struct sembuf sem_b;
    sem_b.sem_num = 0;
    sem_b.sem_op = -1;
    sem_b.sem_flg = SEM_UNDO;
    semop(sem_id, &sem_b, 1);
    
    1
    2
    3
    4
    5
    6
    
    // 将信号量的值+1
     struct sembuf sem_b;
     sem_b.sem_num = 0;
     sem_b.sem_op = 1;
     sem_b.sem_flg = SEM_UNDO;
     semop(sem_id, &sem_b, 1);
    

其他操作命令

  • ipcs -s可以查看系统的信号量,内容有键值(key),信号量编号(semid),创建者(owner),权限(perms),信号量数(nsems)

    1
    2
    3
    4
    
    ➜  c ipcs -s
    
    --------- 信号量数组 -----------
    键        semid      拥有者  权限     nsems  
    
  • ipcrm sem + 信号量编号,可以手工删除信号量

POSIX信号量的使用

头文件

1
#include <semaphore.h>

信号量的类型——sem_t

1
typedef int sem_t;

sem_init函数

  • 作用:初始化信号量

  • 函数原型:

    1
    
    int sem_init(sem_t *sem, int pshared, unsigned int value);
    
  • 参数:
    • sem : 信号量变量的地址
    • pshared : 0 用在线程间 ,非0用在进程间
    • value : 信号量中初始化的值
  • 返回值:
    • 成功:返回0
    • 失败:返回-1,并设置errno 错误代码:
      • EINVAL:设定的值超过SEM_VALUE_MAX
      • ENOSYSpshared非零,但系统不支持进程共享

sem_destroy函数

  • 作用:释放指向的地址处的无名信号量资源(只能释放由sem_init初始化的信号量)

  • 函数原型:

    1
    
    int sem_destroy(sem_t *sem);
    
  • 参数:

    • sem:sem_init函数中初始化的信号量地址
  • 返回值:

    • 成功:返回0
    • 失败:返回-1,并设置errno 错误代码:
      • EINVALsem不是有效的信号量

sem_wait函数

  • 作用:对信号量加锁,调用一次对信号量的值-1,如果值为0,就阻塞(也就是P操作)

  • 函数原型:

    1
    
    int sem_wait(sem_t *sem);
    
  • 参数:

    • sem:sem_init函数中初始化的信号量地址
  • 返回值:

    • 成功:返回0
    • 失败:返回-1,并设置errno 错误代码 错误码如下:
      • EINTR: 呼叫被信号处理程序打断
      • EINVALsem不是有效的信号量
      • ETIMEDOUT:超时

sem_trywait函数

  • 作用:与sem_wait相同,但不阻塞等待,如果不能执行则直接返回错误

  • 函数原型:

    1
    
    int sem_trywait(sem_t *sem);
    
  • 参数:

    • sem:sem_init函数中初始化的信号量地址
  • 返回值:

    • 成功:返回0
    • 失败:返回-1,并设置errno 错误代码为EAGAIN

sem_timedwait函数

  • 作用:与sem_wait函数相同,但阻塞时间有限制,如果超过设定阻塞时间仍不能执行则返回错误

  • 函数原型:

    1
    
    int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
    
  • 参数:

    • sem:sem_init函数中初始化的信号量地址

    • abs_timeout:timespec类型的指针,里面填充的数据是限制的阻塞时间,timespec结构如下:

      1
      2
      3
      4
      
      struct timespec {
          time_t tv_sec;      /* Seconds */
          long   tv_nsec;     /* Nanoseconds [0 .. 999999999] */
      };
      
  • 返回值:

    • 成功:返回0
    • 失败:返回-1,并设置errno 错误代码,如果是超时则设置为ETIMEDOUT

sem_post函数

  • 作用:对信号量解锁,调用一次对信号量的值+1
  • 函数原型:

    1
    
    int sem_post(sem_t *sem);
    
  • 参数:

    • sem:sem_init函数中初始化的信号量地址
  • 返回值:

    • 成功:返回0
    • 失败:返回-1,并设置errno 错误代码 错误码:
      • EINVAL:sem不是有效的信号量
      • EOVERFLOW:信号量的最大允许值将被超过

sem_getvalue函数

  • 作用:获取信号量当前的值,并将这个值保存到参数中的地址中

  • 函数原型:

    1
    
    int sem_getvalue(sem_t *sem, int *sval);
    

    使用需链接pthread库

  • 参数:

    • sem:sem_init函数中初始化的信号量地址
    • sval:存储信号值的地址
  • 返回值:

    • 成功:返回0
    • 失败:返回-1,并设置errno 错误代码EINVAL (sem不是有效的信号量)

示例程序

使用多线程完成生产者-消费者模型

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>
#include <semaphore.h>

// 创建一个互斥量
pthread_mutex_t mutex;
// 创建两个信号量
sem_t psem;
sem_t csem;

struct Node{
    int num;
    struct Node *next;
};

// 头结点
struct Node * head = NULL;

void * producer(void * arg) {

    // 不断的创建新的节点,添加到链表中
    while(1) {
        sem_wait(&psem);
        pthread_mutex_lock(&mutex);
        struct Node * newNode = (struct Node *)malloc(sizeof(struct Node));
        newNode->next = head;
        head = newNode;
        newNode->num = rand() % 1000;
        printf("add node, num : %d, tid : %ld\n", newNode->num, pthread_self());
        pthread_mutex_unlock(&mutex);
        sem_post(&csem);
    }

    return NULL;
}

void * customer(void * arg) {

    while(1) {
        sem_wait(&csem);
        pthread_mutex_lock(&mutex);
        // 保存头结点的指针
        struct Node * tmp = head;
        head = head->next;
        printf("del node, num : %d, tid : %ld\n", tmp->num, pthread_self());
        free(tmp);
        pthread_mutex_unlock(&mutex);
        sem_post(&psem);
       
    }
    return  NULL;
}

int main() {

    pthread_mutex_init(&mutex, NULL);
    sem_init(&psem, 0, 8);
    sem_init(&csem, 0, 0);

    // 创建5个生产者线程,和5个消费者线程
    pthread_t ptids[5], ctids[5];

    for(int i = 0; i < 5; i++) {
        pthread_create(&ptids[i], NULL, producer, NULL);
        pthread_create(&ctids[i], NULL, customer, NULL);
    }

    for(int i = 0; i < 5; i++) {
        pthread_detach(ptids[i]);
        pthread_detach(ctids[i]);
    }

    while(1) {
        sleep(10);
    }

    pthread_mutex_destroy(&mutex);

    pthread_exit(NULL);

    return 0;
}
0%