Linux进程通信之信号量

虽然本文是记录使用信号量保证进程的同步与互斥的,但是其实也可以看做是进程之间的通信问题,为了与前面的保持一致,所以还是叫做 Linux进程间通信了!

信号量

基本概念

进程间通信的方式有管道、消息队列、共享内存这些都是进程间的信息通信,而信号量可以理解为进程使用的临界资源的状态说明,信号量主要用于保证同步与互斥

  • 临界资源:两个进程看到的一份公共资源称之为临界资源
  • 临界区:各个进程中访问临界资源的代码叫做临界区
  • 互斥:每个进程访问临界资源的时候必须是独占式的(排他式的),只能自己一个人访问
  • 同步:防止不间断的占有资源和释放资源,这样的话其他进程就会长时间得不到资源,这样会造成进程的饥饿问题

由此可见我们之前用于进程间通信的管道,消息队列,共享内存都是临界资源,管道是内核已经提供了同步与互斥,但是消息队列和共享内存都是不保证同步与互斥的

信号量PV原语

信号量:本质上是一把计数器 如果一个信号只有0或者1,那么这个就是二元信号量,所以二元信号量可以实现互斥锁

P操作:计数器 -- V操作:计数器 ++ 信号量本身也是临界资源,所以P、V操作必须是原子的

信号量集结构

 1struct ipc_perm { 
 2       key_t __key;    // 提供给 semget()的键 
 3       uid_t uid;      // 所有者有效 UID  
 4       gid_t gid;      // 所有者有效 GID 
 5       uid_t cuid;     // 创建者有效 UID 
 6       gid_t cgid;     // 创建者有效 GID
 7       unsigned short mode;     // 权限 
 8       unsigned short __seq;    // 序列号
 9}; 
10//信号量集的结构
11struct semid_ds {
12	struct ipc_perm sem_perm;   // 所有者和权限
13	time_t sem_otime;           // 上次执行semop的时间  
14	time_t sem_ctime;           // 上次更新时间 
15	unsigned short sem_nsems;   // 在信号量集合里的索引
16}

信号量API

semget

作用:用于创建信号量集

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

key 信号集的名字,这个与再创建管道、消息队列、共享内存等用的key是一致的

nsems 信号集中信号量的个数,一般为1(信号集底层就是数组)

semflg 同创建消息队列等一样的权限,sem_flags取两个值,IPC_CREATEIPC_EXCL,需要配权限使用

  • IPC_CREATE 表示若信号量已存在,返回该信号量标识符
  • IPC_EXCL 表示若信号量已存在,返回错误

return 成功返回一个非负整数,即该信号集的标识码;失败返回-1

num_sems:信号量的数目,

shmctl

作用:用于控制信号量集

1#include <sys/ipc.h>
2#include <sys/shm.h>
3
4int shmctl(int shmid, int semnum, int cmd, ...);

shmid 这个就是要控制的信号量集

semnum 这个是具体要控制的信号量,因为shmid只能指明是哪一个信号量集(数组),而semnum就是数组下标

cmd 将要采取的动作(有三个可取值)

  • SETVAL (常用) 用来把信号量初始化为一个已知的值。p这个值通过union semun中的val成员设置,其作用是在信号量第一次使用的时候
  • GETVAL 获取信号量集中的信号量计数值
  • IPC_STAT 把semid_ds结构中的数据设置为信号量集的当前关联值
  • IPC_SET 在进程有足够权限的情况下,把信号量集的当前关联值设置为semid_ds数据结构中给出的值
  • IPC_RMID (常用) 删除信号量集

semop

作用:修改信号量集中的值

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

semid 这个就是要修改的信号量集

sops 如下结构体的指针,这个结构体是这样的:

1struct sembuf{  
2    short sem_num;//除非使用一组信号量,否则它为0  
3    short sem_op;//信号量在一次操作中需要改变的数据,通常是两个数,一个是-1,即P(等待)操作,一个是+1,即V(发送信号)操作。  
4    short sem_flg;//通常为SEM_UNDO,使操作系统跟踪信号,并在进程没有释放该信号量而终止时,操作系统释放信号量
5}; 

nsops 信号量的个数

return 成功返回0,失败返回1

信号量使用示例

makefile

1test:comm.c main.c
2	gcc -o $@ $^
3
4.PHONY:clean
5clean:
6	rm -rf $@

comm.c && comm.h && main.c

  1#ifndef __COMM_H__
  2#define __COMM_H__
  3
  4#include <stdio.h>
  5#include <sys/types.h>
  6#include <sys/ipc.h>
  7#include <sys/sem.h>
  8#include <unistd.h>
  9#include <wait.h>
 10
 11#define PATHNAME "."
 12#define PROJ_ID 0x6666
 13
 14union semun {
 15  int val; /* Value for SETVAL */
 16  struct semid_ds *buf; /* Buffer for IPC_STAT, IPC_SET */
 17  unsigned short *array; /* Array for GETALL, SETALL */
 18  struct seminfo *__buf; /* Buffer for IPC_INFO */
 19};
 20
 21int createSemSet(int nums);
 22
 23int initSem(int semid, int nums, int initVal);
 24
 25int getSemSet(int nums);
 26
 27int P(int semid, int who);
 28
 29int V(int semid, int who);
 30
 31int destorySemSet(int semid);
 32
 33#endif //!__COMM_H__
 34
 35
 36//---------------------comm.c----------------------------
 37
 38#include "comm.h"
 39
 40static int commSemSet(int nums, int flags){
 41  key_t _key = ftok(PATHNAME, PROJ_ID);
 42  if(_key < 0){
 43    perror("ftok");
 44    return -1;
 45  }
 46
 47  int semid = semget(_key, nums, flags);
 48  if(semid < 0){
 49    perror("semget");
 50    return -2;
 51  }
 52  return semid;
 53}
 54
 55int createSemSet(int nums){
 56  return commSemSet(nums, IPC_CREAT|IPC_EXCL|0666);  
 57}
 58
 59int getSemSet(int nums){
 60  return commSemSet(nums, IPC_CREAT);
 61}
 62
 63int initSem(int semid, int nums, int initVal){
 64  union semun _un;
 65  _un.val = initVal;
 66  if(semctl(semid, nums, SETVAL, _un) < 0){
 67    perror("semctl");
 68    return -1;
 69  }
 70  return 0;
 71}
 72
 73static int commPV(int semid, int who, int op){
 74  struct sembuf _sf;
 75  _sf.sem_num = who;
 76  _sf.sem_op = op;
 77  _sf.sem_flg = 0;
 78
 79  if(semop(semid, &_sf, 1) < 0){
 80    perror("semop");
 81    return -1;
 82  }
 83  return 0;
 84}
 85
 86int P(int semid, int who){
 87  return commPV(semid, who, -1);
 88}
 89
 90int V(int semid, int who){
 91  return commPV(semid, who, 1);
 92}
 93
 94int destorySemSet(int semid){
 95  int ret = semctl(semid, 0, IPC_RMID);
 96  if(ret < 0){
 97    perror("semctl");
 98    return -1;
 99  }
100  return ret;
101}
102//----------------------------main.c----------------------
103#include "comm.h"
104
105int main(){
106  int semid = createSemSet(1);
107  initSem(semid, 0, 1);
108
109  pid_t id = fork();
110
111  if(id == 0){
112    int _semid = getSemSet(0);
113    while(1){
114      //P(_semid, 0);
115      printf("A");
116      fflush(stdout);
117      usleep(100000);
118      printf("A");
119      fflush(stdout);
120      usleep(100000);
121      //V(_semid, 0);
122    }
123  }
124  else{
125    while(1){
126      //P(semid, 0);
127      printf("B");
128      fflush(stdout);
129      usleep(100000);
130      printf("B");
131      fflush(stdout);
132      usleep(100000);
133      //V(semid, 0);
134    }
135    wait(NULL);
136  }
137
138  destorySemSet(semid);
139  return 0;
140}

打开PV操作时与未打开时的对比: mark 同样的使用ipcs -s命令即可查看信号量,使用ipcrm -s 即可释放信号量资源

进程间通信总结

管道

  • 数据只能向一个方向流动;需要双方通信时,需要建立起两个管道
  • 匿名管道只能用于具有亲缘关系的进程,否则使用命名管道
  • 管道内部保证同步机制,从而保证访问数据的一致性。
  • 管道是面向字节流的
  • 管道生命周期随进程,进程在管道在,进程消失管道对应的端口也关闭,两个进程都消失管道也消
  • 管道读端关闭,操作系统向写端发信号终止写端进程
  • 每写一块数据的最大长度是有上限的

System V

消息队列

  • 消息队列提供了一个从一个进程向另外一个进程发送一块数据的方法
  • 每个数据块都被认为是有一个类型,接收者进程接收的数据块可以有不同的类型值
  • 每写一块数据的最大长度是有上限的,这点与管道一致
  • 每个消息队列的总的字节数是有上限的(MSGMNB),系统上消息队列的总数也有上限
  • 消息队列生命周期随内核
  • 不保证同步与互斥

共享内存

  • 共享内存区是最快的IPC形式,无需内核干预,直接映射同一块物理内存
  • 作为IPC资源存在,共享内存生命周期同样随内核
  • 不保证同步与互斥

信号量

  • 主要提供对进程间共享资源访问控制机制。相当于内存中的标志,进程可以根据它判定是否能够访问某些共享资源,同时,进程也可以修改该标志。除了用于访问控制外,还可用于进程同步。
  • 二元信号量:最简单的信号量形式,信号灯的值只能取0或1,类似于互斥锁

Socket

  • 两台计算机相互通信本质上也是两个不在同一个计算机上的进程之间的通信(事实上本机之间的进程通过Socket通信也属于这个范畴),以后再说!