Linux 环境下,进程地址空间相互独立,每个进程各自有不同的用户地址空间。任何一个进程的全局变量在另一个进程中都看不到,所以进程和进程之间不能相互访问,要交换数据必须通过内核,在内核中开辟一块缓冲区,进程 1 把数据从用户空间拷到内核缓冲区,进程 2 再从内核缓冲区把数据读走,内核提供的这种机制称为进程间通信(IPC,InterProcess Communication)

进程间通信概述

管道

在学 Linux 命令时就有管道在这个概念,比如下面这个命令

ps -ef  | -grep root | xargs kill -9

将上一个命令的输出作为下一个命令的输入,数据只能向一个方向流动;双方需要互相通信时,需要建立起两个管道。

管道有两种类型:匿名管道和命名管道。上面提到的命令中|表示的管道即匿名管道 pipe。用完即销毁,自动创建,自动销毁。

使用mkfifo显示创建的是命名管道 fifo

mkfifo hello

hello即是管道名称,类型为p,就是pipe,接下来就可以在管道里写入东西,

# echo "hello world" > hello

光写入还不行,只有有另一个进程读取了内容才完成一次信息交换,才完成一次通信,

# cat < hello 
hello world

这种方式通信效率低,无法频繁通信。

消息队列

类似于日常沟通使用的邮件,有一定格式,有个收件列表,列表上的用户都可以反复在原邮件基础上回复,达到频繁交流的目的。这种模型就是消息队列模型

共享内存

共享内存允许两个或多个进程共享一个给定的存储区,这一段存储区可以被两个或两个以上的进程映射至自身的地址空间中,一个进程写入共享内存的信息,可以被其他使用这个共享内存的进程,通过一个简单的内存读取错做读出,从而实现了进程间的通信。

每个进程都有自己独立的虚拟内存空间,不同进程的虚拟内存空间映射到不同的物理内存中去。这个进程访问 A 地址和另一个进程访问 A 地址,其实访问的是不同的物理内存地址,对于数据的增删查改互不影响。

但是,咱们是不是可以变通一下,拿出一块虚拟地址空间来,映射到相同的物理内存中。这样这个进程写入的东西,另外一个进程马上就能看到了,都不需要拷贝来拷贝去,传来传去。

使用shmget函数创建一个共享内存,

//key_t key:  唯一定位一个共享内存对象
//size_t size: 共享内存大小
//int flag: 如果是 IPC_CREAT 表示创建新的共享内存空间
int shmget(key_t key, size_t size, int flag);

创建完毕之后,我们可以通过 ipcs 命令查看这个共享内存。

#ipcs ­­--shmems
 
------ Shared Memory Segments ------ ­­­­­­­­
key        shmid    owner perms    bytes nattch status
0x00000000 19398656 marc  600    1048576 2      dest

进程通过shmat,就是attach的意思,将内存加载到自己虚拟地址空间某个位置。

//int shm_id:
//const void *addr: 加载的地址,通常设为 NULL,让内核选一个合适地址
//int flag:
void *shmat(int shm_id, const void *addr, int flag);

如果共享内存使用完毕,可以通过 shmdt 解除绑定,然后通过 shmctl,将 cmd 设置为 IPC_RMID,从而删除这个共享内存对象。

int shmdt(void *addr); 
int shmctl(int shm_id, int cmd, struct shmid_ds *buf);

共享内存的最大不足之处在于,由于多个进程对同一块内存区具有访问的权限,各个进程之间的同步问题显得尤为突出。必须控制同一时刻只有一个进程对共享内存区域写入数据,否则将造成数据的混乱。

信号量

如果两个进程同时向一个共享内存读写数据,很可能就会导致冲突。所以需要有一种保护机制,使得同一个共享资源同时只能被一个进程访问。在进程间通信机制中,信号量(Semaphore)就是用来实现进程间互斥与同步的。它其实是个计数器,只不过不是用来记录进程间通信数据的。

我们可以将信号量初始化为一个数值,来代表某种资源的总体数量。对于信号量来讲,会定义两种原子操作,一个是P 操作,我们称为申请资源操作。这个操作会申请将信号量的数值减去 N,表示这些数量被他申请使用了,其他人不能用了。另一个是V操作,我们称为归还资源操作,这个操作会申请将信号量加上 M,表示这些数量已经还给信号量了,其他人可以使用了。

所谓原子操作(Atom Operation)就是不可被中断的一个或一系列操作。

使用semget创建信号量,第一个参数表示唯一标识,第二个参数表示可以创建多少个信号量。

int semget(key_t key, int num_sems, int sem_flags);

接下来,我们需要初始化信号量的总的资源数量。通过semctl 函数,第一个参数 semid是这个信号量组的id,第二个参数 semnum 才是在这个信号量组中某个信号量的id,第三个参数是命令,如果是初始化,则用 SETVAL,第四个参数是一个 union。如果初始化,应该用里面的val设置资源总量。

int semctl(int semid, int semnum, int cmd, union semun args);
 
 
union semun
{
  int val;
  struct semid_ds *buf;
  unsigned short int *array;
  struct seminfo *__buf;
};

无论是 P 操作还是 V 操作,我们统一用 semop 函数。第一个参数还是信号量组的 id,一次可以操作多个信号量。第三个参数 numops 就是有多少个操作,第二个参数将这些操作放在一个数组中。

数组的每一项是一个 struct sembuf,里面的第一个成员是这个操作的对象是哪个信号量。第二个成员就是要对这个信号量做多少改变。如果 sem_op < 0,就请求 sem_op 的绝对值的资源。如果相应的资源数可以满足请求,则将该信号量的值减去 sem_op 的绝对值,函数成功返回。

当相应的资源数不能满足请求时,就要看sem_flg 了。如果把 sem_flg 设置为IPC_NOWAIT,也就是没有资源也不等待,则 semop 函数出错返回 EAGAIN。如果 sem_flg 没有指定IPC_NOWAIT,则进程挂起,直到当相应的资源数可以满足请求。若 sem_op > 0,表示进程归还相应的资源数,将 sem_op 的值加到信号量的值上。如果有进程正在休眠等待此信号量,则唤醒它们。

int semop(int semid, struct sembuf semoparray[], size_t  numops);
struct sembuf 
{
  short sem_num; // 信号量组中对应的序号,0~sem_nums-1
  short sem_op;  // 信号量值在一次操作中的改变量
  short sem_flg; // IPC_NOWAIT, SEM_UNDO
}

信号

以上提到的通信方式,都是常规状态下的工作模式,而信号一般是由错误产生的。

信号没有特别复杂的数据结构,就是用一个代号一样的数字。Linux 提供了几十种信号,分别代表不同的意义。信号之间依靠它们的值来区分。