lINUX进程之间通信主要的方式:管道、消息队列、内存共享、信号量、信号、SOCKET本地通信。每个进程的用户地址空间都是独立的,一般而言是不能互相访问的,但内核空间是每个进程都共享的,所以进程之间要通信必须通过内核。
一、管道
如果你学过 Linux 命令,那你肯定很熟悉「|」这个竖线。
ps -ef | grep java
上面命令行里的”|”,竖线就是一个管道,它的功能是将前一个命令(ps -f)的输出,作为后一个命令(grep java)的输入,从这功能描述,可以看出管道传输数据是单向的,如果想相互通信,我们需要创建两个管道才行。而在这里的”|”表示的管道并没有名字,因此被较为匿名管道,数据传输完成后将自动销毁。
有匿名的管道,自然就还有有名字的,叫命名管道。数据采取先进先出的方式传输。命名管道我们可以通过shell进行创建:
$ mkfifo myPipeline
在LInux系统中,一切都是文件,因此命名管道将也是以文件的方式存在,创建后在当前目录可以查看到相应文件。创建后,就可以向管道myPipeline写入数据,并在其他进程中取出该数据了
但是输入数据的进程,在数据没有被读写时候,会被阻塞,直到管道中的数据被其他进程读取后才会返回
因此可以直到,这种管道的方式虽然使用简便,但会阻塞进程,效率必然很低,因此管道不适合进程间频繁地交换数据。
通过管道交换数据的方式,还有一个必要条件,就是交换数据的两个进程需要有1个共同的父进程,因此他还有个缺点就是他并不能在任意的进程之间进行数据交换。
例如上面的例子,两个shell命令终端实际上都是shell进程的子进程,因此相互之间可以交换数据。
匿名管道交换数据的原理是一样的,只是他不会在系统中创建1个管道文件,而是直接在内核空间开辟1个缓存区域区域,进行进程数据的交换。匿名管道只可用于父子进程之间的数据通信,因为只有通过frok父进程,子进程才能得到父进程创建匿名管道的文件描述符
二、消息队列
消息队列和我们常用的消息队列中间件等类似,可以满足数据的多进多出,单个进程对数据的收发都不会被阻塞和影响其他进程,发送或(注意是或者不是和)读取数据完成后都会立即返回。
消息队列是保存在内核中的消息链表,在发送数据时,会分成一个一个独立的数据单元,也就是消息体(数据块),消息体是用户自定义的数据类型,消息的发送方和接收方要约定好消息体的数据类型,所以每个消息体都是固定大小的存储块,不像管道是无格式的字节流数据。如果有其他进程从消息队列中读取了消息体,内核就会把这个消息体删除。
消息队列生命周期随内核,如果没有释放消息队列或者没有关闭操作系统,消息队列会一直存在,而前面提到的匿名管道的生命周期,是随着进程的创建而建立,随进程的结束而销毁。
消息队列虽然可以多方沟通,但实际上并没有应答机制,也就是说你发送了数据并不一定有任何回应,另外因为消息队列是固定数据块,所以单条消息的长度和一个队列的长度都是有限制的,他也不适合传输大文件或者二进制流。顾名思义,他适合交换简单的消息型数据。
因为消息队列涉及到用户态内存到内核态内存空间的数据拷贝, 所以他是比较消耗CPU性能的。
三、共享内存
简单来讲:共享内存就是只不同进程在内核态共享一块固定的内存区域可以进行读取和数据的写入,从来形成不同进程之间的数据交换。
这个方式多个进程之间可以直接读取数据,将大大提高通信的效率。但共享的东西,大家知道就像共享单车,可能存在大家都想真枪这用的情况。因此就有了通过信号量来管理共享内存的手段,理论上共享内存和信号量必定是同时使用。
共享内存的生命周期随内核。即所有访问共享内存区域对象的进程都已经正常结束,共享内存区域对象仍然在内核中存在(除非显式删除共享内存区域对象),在内核重新引导之前,对该共享内存区域对象的任何改写操作都将一直保留;简单地说,共享内存区域对象的生命周期跟系统内核的生命周期是一致的,而且共享内存区域对象的作用域范围就是在整个系统内核的生命周期之内。
1、所有的函数共用头文件:
#include
#include
#include
2、创建共享内存——>shmget() 函数
int shmget(key_t key, size_t size, int shmflg);
Key可以认为是这块共享内存区域的全局唯一标识符号;
Size是开辟的内存大小,它的值一般为一页大小的整数倍(未到一页,操作系统向上对齐到一页,但是用户实际能使用只有自己所申请的大小)
shmflg是一组标志,创建一个新的共享内存,将shmflg 设置了IPC_CREAT标志后,共享内存存在就打开。而IPC_CREAT | IPC_EXCL则可以创建一个新的,唯一的共享内存,如果共享内存已存在,返回一个错误。
如果创建共享内存成功则返回共享内存的ID,出错返回-1
3、操作共享内存———>shmctl()函数
int shmctl(int shm_id, int cmd, struct shmid_ds *buf);
shm_id是shmget函数返回的共享内存标识符。
cmd是要采取的操作,它可以取下面的三个值 :
IPC_STAT:把shmid_ds结构中的数据设置为共享内存的当前关联值,即用共享内存的当前关联值覆盖shmid_ds的值。
IPC_SET:如果进程有足够的权限,就把共享内存的当前关联值设置为shmid_ds结构中给出的值。
IPC_RMID:删除共享内存段
buf是一个结构指针,它指向共享内存模式和访问权限的结构。 shmid_ds结构定义如下:
struct shmid_ds
{
uid_t shm_perm.uid;
uid_t shm_perm.gid;
mode_t shm_perm.mode;
};
4、挂接操作———>shmat()函数
创建共享存储段之后,将进程连接到它的地址空间
void *shmat(int shm_id, const void *shm_addr, int shmflg);
功返回指向共享存储段的指针,出错返回-1。
shm_id是由shmget函数返回的共享内存标识。
shm_addr指定共享内存连接到当前进程中的地址位置,通常为空,表示让系统来选择共享内存的地址。
shm_flg是一组标志位,通常为0
5、分离操作———>shmdt()函数
该操作不从系统中删除标识符和其数据结构,要显示调用shmctl(带命令IPC_RMID)才能删除它。仅仅是删除挂接,其他状态保持不变。
int shmdt(const void *shmaddr);
shmaddr是内存的地址。
四、信号量
信号量就是一个整型的计数器,用于实现进程间的互斥与同步,信号量本身不用于进程简单的数据通信,对他的访问具有原子性。
信号量表示资源的数量,控制信号量的方式有两种原子操作:
1、一个是 P 操作,这个操作会把信号量减去 -1,相减后如果信号量 < 0,则表明资源已被占用,进程需阻塞等待;相减后如果信号量 >= 0,则表明还有资源可使用,进程可正常继续执行。
2、另一个是 V 操作,这个操作会把信号量加上 1,相加后如果信号量 <= 0,则表明当前有阻塞中的进程,于是会将该进程唤醒运行;相加后如果信号量 > 0,则表明当前没有阻塞中的进程;
P 操作是用在进入共享资源之前,V 操作是用在离开共享资源之后,这两个操作是必须成对出现的。
另外信号量还可以帮助进程之间进行数据同步操作。典型的就是生产者进程和消费者进程之间的数据同步过程:
首先初始化信号量为 0
1、如果进程 B 比进程 A 先执行了,那么执行到 P 操作时,由于信号量初始值为 0,故信号量会变为 -1,表示进程 A 还没生产数据,于是进程 B 就阻塞等待;
2、接着,当进程 A 生产完数据后,执行了 V 操作,就会使得信号量变为 0,于是就会唤醒阻塞在 P 操作的进程 B;
3、最后,进程 B 被唤醒后,意味着进程 A 已经生产了数据,于是进程 B 就可以正常读取数据了。
如果需要做互斥操作,初始化信号量为1,如果要做同步操作,则初始化型号两为0。\
信号量在LINUX系统中的简单使用:
Linux提供了一组信号量API,声明在头文件sys/sem.h中。
1、semget函数:新建信号量
int semget(key_t key,int num_sems,int sem_flags);
key:信号量键值,可以理解为信号量的唯一性标记。
num_sems:信号量的数目,一般为1
sem_flags:有两个值,IPC_CREATE和IPC_EXCL,
IPC_CREATE表示若信号量已存在,返回该信号量标识符。
IPC_EXCL表示若信号量已存在,返回错误。
返回值:则返回相应的信号量标识符,如果失败则返回-1
2、semop函数:修改信号量的值
int semop(int sem_id,struct sembuf *sem_opa,size_t num_sem_ops);
sem_id:信号量标识符
sem_opa:描述信号量的结构体
结构体定义
struct sembuf{
short sem_num;//除非使用一组信号量,否则它为0
short sem_op;//操作指令 -1表示P,+1表示V
short sem_flg;//通常为SEM_UNDO,使操作系统跟踪信号, 并在进程没有释放该信号量而终止时,操作系统释放信号量
};
3、semctl函数:用于信号量的初始化和删除
int semctl(int sem_id,int sem_num,int command,[union semun sem_union]);
command:有两个值SETVAL,IPC_RMID,分别表示初始化和删除信号量。
sem_union: 共同体,定义如下
union semun{
int val; //这个表示创建信号量时的初始值 ,一般1用于互斥 0用于同步
struct semid_ds *buf;
unsigned short *arry;
};
五、信号
上面说的进程间通信,都是进程处于常规状态下的工作模式,对于异常的工作模式下,则需要用信号的方式来进行处理了。
在 Linux 操作系统中, 为了响应各种各样的事件,提供了几十种信号,分别代表不同的意义。我们可以通过 kill -l 命令,查看所有的信号:
运行在 shell 终端的进程,我们可以通过键盘输入某些组合键的时候,给进程发送信号。例如:
Ctrl+C 产生 SIGINT 信号,表示终止该进程;
Ctrl+Z 产生 SIGTSTP 信号,表示停止该进程,但还未结束;
如果进程在后台运行,可以通过 kill 命令的方式给进程发送信号,但前提需要知道运行中的进程 PID 号,例如:
kill -9 1050 ,表示给 PID 为 1050 的进程发送 SIGKILL 信号,用来立即结束该进程;
所以,信号事件的来源主要有硬件来源(如键盘 Cltr+C )和软件来源(如 kill 命令)。
信号是进程间通信机制中唯一的异步通信机制,因为可以在任何时候发送信号给某一进程,一旦有信号产生,我们就有下面这几种,用户进程对信号的处理方式。
1、执行默认操作。Linux 对每种信号都规定了默认操作,例如,上面列表中的 SIGTERM 信号,就是终止进程的意思。Core 的意思是 Core Dump,也即终止进程后,通过 Core Dump 将当前进程的运行状态保存在文件里面,方便事后进行分析问题在哪里。
2、捕捉信号。我们可以为信号定义一个信号处理函数。当信号发生时,我们就执行相应的信号处理函数。
3、忽略信号。当我们不希望处理某些信号的时候,就可以忽略该信号,不做任何处理。有两个信号是应用进程无法捕捉和忽略的,即 SIGKILL 和 SEGSTOP,它们用于在任何时候中断或结束某一进程。
特别说明,进程之间的信号特指的是linux系统定义的64个特定系统调用事件,与glib的信号、qt的信号槽这些是没有关系的。进程内部的信号只是一种回调机制,不会切换内核的上下文。当然gsignal对来自系统的信号也是进行了注册和封装的。
六、SOCKET
我们知道,SOCKET可以用于网络之间不同主机的通信。都是不通主机了,那肯定是不同的进程。当然既然可以在不同主机的不同进程间通信,那肯定也可以在同一主机间的不同进程之间进行通信。
只是使用时候有所区别,不同主机间是通过IP地址和通信端口进行识别的,而同一主机的两个进程之间通过socket通信,除了仍然可以用本地IP及端口进行通信外,还可以通过AF_LOCAL\AF_UNIX通信。这种本地通信的时候,socket将在本地绑定一个文件进行交换。这个和命名管道的方式类似,但这个文件是1个特殊的看不到摸不到的文件,系统将以这个文件描述符号为多个进程之间通信的唯一标识。本地通信的流程如下:
应用层-> socket接口 -> 传输层(tcp/udp报文) -> 网络层 -> back to 传输层 -> backto socket接口 -> 传回应用程序。
除了不会真正走到网络层去传输数据外,其他高层的路径和流程与网络间通信的模式无差异。
给予AF_UNIX的进程间通信,Linux中netstat 命令仍然可以查询到,协议字段将显示为UNIX