Linux进程通信
由于进程采用的是虚拟空间+用户态/内核态机制,所以就导致进程与进程之间、进程与内核之间是互相独立的,如果想让多个进程协同工作解决复杂问题,就需要进程之间进行通信。
一、简单的进程间通信
命令行参数:
情况1:在终端执行程序时,给子进程传递命令行参数。
情况2:使用vfork+exec创建进程时,给子进程传递命令行参数。
环境变量表:
情况1:使用fork创建子进程时,子进程会拷贝一份父进程的环境变量表。
情况2:使用vfork+exec创建进程时,给子进程传递一份父进程的环境变量表,子进程拿到环境表后会进行拷贝。
只能在父子进程之间,父进程创建子进程时使用,只能由父进行传递给子进程,单向通信,并且只能传递一些简单的字符串数据。
信号:
情况1:使用kill向指定的进程发送信号进行通信。
情况2:使用sigqueue向指定的进程发送信号,也可以附带一些简单的数据。
可以在任意进程之间进行通信,但也只能是互相告知某某事件发生了,即使能使用sigqueue带一些数据,但只能传递一个int类型整数,只有通过fork创建的子进程才能传递内存地址。
文件:
情况1:使用文件+信号,让任意两个进程之间传递大量数据,但要各自控制好位置指针,协调好读写时间。
情况2:使用文件+文件锁,也可以让任意两个进程之间传递大量数据,但文件锁的操作比较麻烦,也有可能陷入死锁的情况。
单纯的使用文件进行通信,无法协调读取和写入的时间,空间造成文件内的数据混乱,所以必须配合使用信号或文件锁。
二、传统的进程间通信
管道是UNIX系统最古老的进程间通信方式,是一种特殊的文件读写机制。
当进程从管道文件读取数据时,如果管道中没有数据则进程会进入阻塞状态,直到读取到数据才返回,所以就不需要使用信号、文件锁协调读写时间。
管道中的数据一旦读取完毕就会消失,不需要管理文件位置指针,所以管道文件通信比直接使用文件进行通信要方便很多。
1、有名(命名)管道:
在文件系统中创建出一个实体的管道文件,然后再使用系统I/O相关API进行相关操作。【半双工】
使用函数创建:
int mkfifo(const char *pathname, mode_t mode);
功能:创建一个管道文件
pathname:管道文件的路径
mode:管道文件的权限
使用命令创建:
mkfifo <filo>
# echo hello > fifo
# cat fifo
2、匿名管道:
只在内核中创建出管道文件对象并返回该对象的文件描述符,然后再使用系统I/O相关API进行相关操作。匿名管道不会在文件系统中显示,在创建时不需要提供路径,也不占用磁盘空间,而只使用内核空间的内存通信,关闭文件描述符后自动回收。
注意:只能用于父子进程(fork创建的)
#include <unistd.h>
int pipe (int pipefd[2]);
功能:创建出管道文件对象并返回该对象的文件描述符
pipefd:存储文件描述符的数组,其中pipefd[0]用于读,pipefd[1]用于写。
返回值:成功返回0,失败返回-1。
使用步骤:
1、调用该函数在内核中创建管道文件,并通过其输出参数,获得分别用于读和写的两个文件描述符
2、调用fork函数,创建子进程,子进程会拷贝pipefd数组。
3、写数据的进程关闭读端(pipefd[0]),读数据的进程关闭写端(pipefd[1])
4、父子进程传输数据
5、父子进程分别关闭自己的文件描述符
三、XSI机制的进程间通信
IPC对象:
它用于进程间通信的XSI-IPC内核对象,就是像匿名管道、文件内核对象一样,使用XSI方式进行进程间通信时,系统会在内核中创建一个XSI-IPC内核对象,让需要通信的进程共同访问。
XSI-IPC只存在于内核空间,不在文件系统显示,通过IPC键进行创建、获取。
IPC键值:
用于创建XSI-IPC内核对象的凭证,是一个无符号整数,相当于IPC对象的名字,类似于文件的文件名。
在创建XSI-IPC内核对象可以由创建者自定义,类似于给文件取名,但所有XSI-IPC内核对象都在同一个作用域下,所以有重名的风险,所以操作系统提供一个自动生成IPC键值的API。
#include <sys/types.h>
#include <sys/ipc.h>
key_t ftok(const char *pathname, int proj_id);
功能:自动生成一个IPC键值
pathname:当前项目的路径,必须一个真实存在的路径,它靠的是路径的位置,而不是字符串。
proj_id:当前项目的编号
注意:只要提供相同的路径和项目编号,就可以获得相同的IPC键值。
IPC描述符:
类似于文件描述符,是一个非负整数,它是IPC内核对象的给用户空间访问凭证,然后这个对象会以整数的形式提供给各个进程。
显示IPC对象命令:
ipcs -m - 显示共享内存(m: memory)
ipcs -q - 显示消息队列(q: queue)
ipcs -s - 显示信号量(s: semphore)
ipcs -a - 显示所有IPC对象(a: all)
删除IPC对象命令:
ipcrm -m ID - 删除共享内存
ipcrm -q ID - 删除消息队列
ipcrm -s ID - 删除信号量
1、共享内存:
基本原理:
在内核中开辟一内存,可以让其它进程的虚拟地址与它进行映射,这样就达到多个进程共享一块内存的目的,当一个进程向这块内存写数据时,其它进程也都可以读取到,这样就达到了通信的目的。
注意:这种通信方式不存在数据的复制,是最快的进程间通信方式。
使用方式:
当一个进程向共享内存写入数据后,内核不会通知其它进程,进程从共享内存读取数据时,无法分辨是否是通信进程新写入的数据,所以有以下三种固定的使用方式。
读写:一个进程只负责写入数据,另一个进程只负责读取数据,只要负责读的进程使用的是最新即可,这是单向通信的模式。
轮询:配合闹钟或定时器,每隔一段时间读取一个。
中断:配合信号,进程只要往里面写入数据就给对方发送一个信号,其它进程如果读取了数据也给对方发送一个已读取的信号。
相关API:
#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);
功能:创建/获取一块内核中共享内存
key:IPC键值,相当于文件名
size:共享内存的字节数,建议取内存页字节数的整数倍,默认一页内存4096字节。
若希望创建共享内存,则必需指定size参数,若只为获取已有的共享内存,则size参数可取0。
shmflg:
0 - 表示该参数无效,获取共享内存,size的值也无效。
IPC_CREAT - 创建共享内存
IPC_EXCL - 互斥,如果共享内存已存在则创建失败
mode - 共享内存的权限,创建共享内存时一定要加
返回值:成功返回共享内存描述符,失败返回-1。
void *shmat(int shmid, const void *shmaddr, int shmflg);
功能:加载共享内存(把进程中的虚拟地址与内核中的共享内存建立映射关系)
shmid:IPC描述符
shmaddr:虚拟地址,也可以为NULL,操作系统会自动分配。
shmflg:
0 - 以读写方式使用共享内存。
SHM_RDONLY - 以只读方式使用共享内存。
SHM_RND - 只在shmaddr参数非NULL时起作用,对shmaddr向下取内存页的整数倍,作为映射地址。
返回值:成功返回映射后的虚拟地址,并且内核将该共享内存的加载计数加1,失败返回-1。
int shmdt(const void *shmaddr);
功能:卸载共享内存,也就是取消映射
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
功能:销毁共享内存、获取共享内存的属性、设置共享内存的属性
shmid:IPC描述符
cmd:
IPC_STAT 获取共享内存的属性
IPC_SET 设置共享内存的属性,uid,gid,mode可以设置。
IPC_RMID 并非真正删除共享内存,只是做一个删除标记,禁止其被继续加载,但已有加载依然保留,只有当该共享内存的加载计数为0时,才真正被删除。
struct shmid_ds {
struct ipc_perm shm_perm; // 所有者及权限
size_t shm_segsz; // 共享内存的大小,以字节为单位
time_t shm_atime; // 最后加载时间
time_t shm_dtime; // 最后卸载时间
time_t shm_ctime; // 最后修改时间
pid_t shm_cpid; // 创建者的PID
pid_t shm_lpid; // 最后加载/卸载的者PID
shmatt_t shm_nattch; // 加载计数器
};
struct ipc_perm
{
key_t __key; // 键值
uid_t uid; // 有效属主ID
gid_t gid; // 有效属组ID
uid_t cuid; // 有效创建者ID
gid_t cgid; // 有效创建组ID
unsigned short mode; // 权限字
unsigned short __seq; // 序列号
};
2、消息队列:
基本原理:
是由系统内核维护的一个链式队列,每个一条消息由类型、数据、数据长度组成。
和管道类似,但它可以双向通信,还可以按类型接收消息,而不是顺序。
相关API:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgget(key_t key, int msgflg);
功能:以key参数为键值 创建或获取消息队列
msgflg:
0 - 获取,不存在即失败。
IPC_CREAT - 创建,不存在即创建,已存在即获取
IPC_EXCL - 排斥,已存在即失败。
mode - 消息队列的权限
返回值:成功返回消息队列标识符,失败返回-1
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
功能:向消息队列发送数据
msgp:向一个包含消息类型和消息内容的内存块,以下是内存块的格式
struct msgbuf
{
long mtype; // 数据的长度,注意不包括消息类型。
char mtext[n]; // 消息内容,也可以使用柔性数组
};
msgsz:消息内容的字节数,不包含消息类型
msgflg:
0 当消息队列中没有足够的空闲空间时,此函数会阻塞。
IPC_NOWAIT 当消息队列中没有足够的空闲空间时,此函数不会阻塞,而是返回-1。
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp,int msgflg);
功能:从消息队列接收数据
msgp:消息内存块的首地址,消息由消息类型+消息数据组成,也叫消息数据缓冲区
msgsz:消息数据缓冲区的字节数,如果实际消息的字符数大于msgsz,则立即返回-1
msgtyp:
>0 - 要接收的消息类型
=0 - 返回消息队列中的第一条消息。
<0 - 返回消息队列中类型小于等于msgtyp的绝对值的消息,如果有多个,则返回类型最小者。
msgflg:
MSG_EXCEPT 返回消息队列中第一个类型不为msgtyp的消息,此时应 msgtyp>0
IPC_NOWAIT 如果没有满足条件的消息,立即返回
MSG_NOERROR 只读取该消息的前msgsz字节,如果有剩余则丢弃
返回值:返回接收到的消息的字节数
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
功能:销毁消息队列、获取消息队列的属性、设置消息队列的属性
cmd取值:
IPC_STAT - 获取消息队列的属性,通过buf参数输出
IPC_SET - 设置消息队列的属性,通过buf参数输入,仅以下四个属性可设置:
msqid_ds::msg_perm.uid
msqid_ds::msg_perm.gid、
msqid_ds::msg_perm.mode
msqid_ds::msg_qbytes
IPC_RMID - 立即删除消息队列同,此时所有阻塞在对该消息队列的,msgsnd和msgrcv函数调用,都会立即返回失败。
struct msqid_ds {
struct ipc_perm msg_perm; // 权限信息
time_t msg_stime; // 随后发送时间
time_t msg_rtime; // 最后接收时间
time_t msg_ctime; // 最后改变时间
unsigned long __msg_cbytes; // 消息队列中的字节数
msgqnum_t msg_qnum; // 消息队列中的消息数
msglen_t msg_qbytes; // 消息队列能容纳的最大字节数
pid_t msg_lspid; // 最后发送进程PID
pid_t msg_lrpid; // 最后接收进程PID
};
struct ipc_perm {
key_t __key; // 键值
uid_t uid; // 有效属主ID
gid_t gid; // 有效属组ID
uid_t cuid; // 有效创建者ID
gid_t cgid; // 有效创建组ID
unsigned short mode; // 权限字
unsigned short __seq; // 序列号
};
3、信号量
基本原理:
所谓信号量就是内核中维护的一个全局变量,被当作计数器,用于限制多个进程对有限共享资源的访问。
1、假定操作系统有n个共享资源,则需要把信号量的值设置为n。
2、有m个进程需要以独占方式使用k个共享资源,且 m*k > n。
3、每个进程在使用共享资源前要尝试执行 信号量-k 操作。
4、如果 信号量-k >= 0,说明剩余的共享资源数量够用,当进程获得该资源后,需执行信号量 -= k,然后尽情的使用共享资源,当不再使用该资源时,需执行信号量 += k。
5、如果信号量 -k < 0,说明剩余的共享资源数量不够用,进程就会阻塞等待该资源,直到信号量-k >= 0,进程被唤醒,执行步骤4;
注意:因为资源的互斥访问,所以很少用到。
相关API:
int semget (key_t key, int nsems, int semflg);
功能:以key参数为键值 创建或获取信号量集
nsems:参数表示集合中的信号量数,或获取已有的信号量集合则nsems取0
semflg取值:
0 - 获取,不存在即失败
IPC_CREAT - 创建,不存在即创建,已存在则获取
IPC_EXCL - 排斥,已存在即失败
mode - 消息队列的权限
返回值:成功返回信号量集合标识,失败返回-1。
int semop (int semid, struct sembuf* sops,unsigned nsops);
功能:对semid参数所标识的信号量集合
sops:struct sembuf结构数组
nsops:结构数组的长度
struct sembuf {
unsigned short sem_num; // 信号量下标
short sem_op; // 操作数
// 若sem_op大于0,则将其加到第sem_num个信号量上,以表示对资源的释放;
// 若sem_op小于0,则将其加到第sem_num个信号量上,以表示对资源的获取;
short sem_flg; // 操作标记
// 0 若第sem_num个信号量的计数值不够减(信号量不能为负),则进程会阻塞,直到该信号量够减为止,以表示对资源的等待;
// IPC_NOWAIT,则当第sem_num个信号量的计数值不够减时,此函数不会阻塞,而是返回-1,以便在等待资源的同时还可做其它处理;
// 若sem_op等0,则直到第sem_num个信号量的计数值为0时才返回,除非sem_flg包含IPC_NOWAIT位。
};
int semctl (int semid, int semnum, int cmd);
int semctl (int semid, int semnum, int cmd,union semun arg);
功能:获取信号量属性、设置信号量属性、删除信号量
cmd取值:
IPC_STAT - 获取信号量集合的属性,通过arg.buf输出。
IPC_SET - 设置信号量集合的属性,通过arg.buf输入,仅以下三个属性可设置:
semid_ds::sem_perm.uid
semid_ds::sem_perm.gid
semid_ds::sem_perm.mode
IPC_RMID立即删除信号量集合。
此时所有阻塞在对该信号量集合的,semop函数调用,都会立即返回失败。
GETALL - 获取信号量集合中每个信号量的计数值,通过arg.array输出。
SETALL - 设置信号量集合中每个信号量的计数值,通过arg.array输入。
GETVAL - 获取信号量集合中,第semnum个信号量的计数值,通过返回值输出。
SETVAL - 设置信号量集合中,第semnum个信号量的计数值,通过arg.val输入。
semnum:
只有针对信号量集合中具体某个信号量的操作,才会使用semnum参数。
针对整个信号量集合的操作,会忽略semnum参数。
union semun {
int val; // Value for SETVAL
struct semid_ds* buf; // Buffer for IPC_STAT, IPC_SET
unsigned short* array; // Array for GETALL, SETALL
struct seminfo* __buf; // Buffer for IPC_INFO
};
struct semid_ds {
struct ipc_perm sem_perm; // Ownership and permissions
time_t sem_otime; // Last semop time
time_t sem_ctime; // Last change time
unsigned short sem_nsems; // No. of semaphores in set
};
struct ipc_perm {
key_t __key; // 键值
uid_t uid; // 有效属主ID
gid_t gid; // 有效属组ID
uid_t cuid; // 有效创建者ID
gid_t cgid; // 有效创建组ID
unsigned short mode; // 权限字
unsigned short __seq; // 序列号
};
struct seminfo {
int semmap; /* Number of entries in semaphoremap; unused within kernel */
int semmni; /* Maximum number of semaphore sets */
int semmns; /* Maximum number of semaphores in all semaphore sets */
int semmnu; /* System-wide maximum number of undo structures; unused within kernel */
int semmsl; /* Maximum number of semaphores in a set */
int semopm; /* Maximum number of operations for semop(2) */
int semume; /* Maximum number of undo entries per process; unused within kernel */
int semusz; /* Size of struct sem_undo */
int semvmx; /* Maximum semaphore value */
int semaem; /* Max. value that can be recorded for semaphore adjustment (SEM_UNDO) */
};