Linux进程间通信

参考:郑谦益. GNU/Linux编程[M]. 北京:人民邮电出版社, 2012 :187-211.

进程通信概述

Linux中,各进程的用户地址空间彼此独立,一个进程不能直接访问另一个进程的用户地址空间,而不同进程之间拥有相同的内核地址空间,Linux内核对于各进程来说是共享的,因此进程之间可以通过内核实现通信。

在没有进程间通信(Inter Process Communication, IPC)之前,可以使用文件、信号或者网络来实现数据交换。两个进程通过文件交换数据,但这种方式操作起来不够方便,且效率较低;使用信号的方式也无法在两个进程间传递大量的信息。后来,引入了管道、IPC和网络套接字的概念和技术用于实现多进程间的同步与通信。

管道(pipe)

管道是在同一台计算机上的两个进程之间进行数据交互的一种机制,具有单向、先进先出、无结构的字节流的特点。管道有两个端点,一端用于写入数据,另一端用于读出数据,当数据从管道中被读出后,这些数据将被移走。

当进程从空管道中读取或写入已满的管道时,进程将被挂起,直到有进程向管道中写入数据或从管道中读取数据。

根据管道提供应用接口的不同,管道可以分为命名管道和无名管道。

无名管道

无名管道是在内核中建立一条管道,管道有两个端,一端用于写,另一端用于读,从管道写入端写入的数据可以从管道的读出端读出。

无名管道的创建

pipe()函数用于创建一条管道,其函数原型如下:

#include <unistd.h>

int pipe(int fd[2]);
  • fd[2]:两个文件描述符,其中fd[0]为读出端,fd[1]为写入端。

返回值:成功返回0,否则返回-1。

应注意管道是单向的,只能从fd[1]写入,从fd[0]读出。两个文件描述符可以像普通文件一样,调用read()、write()和close()等函数操作。

此外,进程使用pipe()函数创建的无名管道的两端所对应的两个文件描述符是属于用户地址空间的,如果其他进程要操作管道的另一端,就必须继承这个文件描述符的资源,因此可以采用父进程创建子进程的方式(fork()函数),分别对管道的一端进行读写。

读写某个进程

popen()函数用于读取某个进程的结果,或写入某个进程的输入,其函数原型如下:

#include <stdio.h>

FILE* popen(const char* command, const char* type);
  • command:要执行的Shell命令;
  • type:执行命令的输入输出类型,"r"表示打开命令执行的标准输出,"w"表示打开命令执行的标准输入。

返回值:成功返回文件I/O流,否则返回NULL。

关闭由popen()函数打开的管道

pclose()函数用于关闭由popen()函数打开的管道,其函数原型如下:

#include <stdio.h>

int pclose(FILE* stream);
  • stream:由popen()打开的管道;

返回值:成功返回exit status,失败返回-1。

命名管道

与无名管道不同,命名管道作为一个特殊的文件保存在文件系统中,任意一个进程都可以对其进行读写,只要该进程有相应的读写权限。

mkfifo()函数用于创建一个管道文件,其函数原型如下:

#include <sys/types.h>
#include <sys/stat.h>

int mkfifo(const char* pathname, mode_t mode);
  • pathname:文件路径名;
  • mode:存取权限。

返回值:成功返回0,失败返回-1,错误信息位于errno中。

消息队列(Message Queue)

消息队列是存在于内核中的消息列表。一个进程可以将消息发送至消息队列,另一个进程则可从消息队列中获取消息。消息队列的操作方式为先进先出。

创建消息队列

msgget()函数用于获取或建立消息队列,其函数原型如下:

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>

int msgget(key_t key, int msgflg);
  • key:用于区分不同消息队列的参数(非0整数);
  • msgflg:存取权限或创建条件。

返回值:成功返回消息队列标识,失败返回-1。

如果key为IPC_PRIVATE,则内核创建一块新的共享内存;否则,如果key所对应的共享内存已存在,则返回该共享内存。msgflg用二进制来标识,其最小9位用于说明共享内存的存取权限,如果msgflg包含IPC_CREAT,则表示创建共享内存。

发送消息

msgsnd()函数用于向已创建的消息队列中放置消息,其函数原型如下:

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>

int msgsnd(int msqid, struct msgbuf* msgp, size_t msgsz, int msgflg);
  • msqid:消息队列标识;
  • msgp:发送给队列的消息,可以是任意类型的结构体,但第一个字段必须是long类型,表明消息的类型,msgrcv()函数将据此接收消息;
  • msgsz:发送消息的长度,不含msgp指示消息类型的前4个字节;
  • msgflg:指定发送消息的行为。

返回值:成功写入返回0,否则返回-1,错误信息位于errno中。

msgflg的取值可以是下面三者其一:

  1. 0:当消息队列满时阻塞函数,直到消息能写入队列;
  2. IPC_NOWAIT:消息队列满时,msgsnd()函数立即返回;
  3. IPC_NOERROR:如果发送的消息长度大于msgsz指示的长度,则做截断处理,截断部分将丢弃,且不通知发送消息的进程。

接收消息

msgrcv()函数用于从消息队列中接收消息,其函数原型如下:

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>

int msgrcv(int msqid, struct msgbuf* msgp, size_t msgsz, long msgtyp, int msgflg);
  • msqid:消息队列标识;
  • msgp:从消息队列接收消息的类型,须与msgsnd()函数发送消息的结构体类型一致;
  • msgsz:接收消息的长度,不含msgp指示消息类型的前4个字节;
  • msgtyp:指示接收消息的行为;
  • msgflg:指示消息类型。

返回值:成功收到消息返回实际收到的消息长度,否则返回-1,错误信息位于errno中。

msgtyp的取值可以是下面三者其一:

  1. 0:接收队列中读端的第一条消息;
  2. >0:接收类型等于msgtyp的第一条消息;
  3. <0:接收类型等于msgtyp绝对值的第一条消息。

msgflg的取值可以是下面四者其一:

  1. 0:当消息队列满时阻塞函数,直到消息能写入队列;
  2. IPC_NOWAIT:消息队列满时,msgsnd()函数立即返回;
  3. IPC_EXCEPT:与msgtyp配合使用时,返回消息队列中第一条类型不为msgtyp的消息;
  4. IPC_NOERROR:如果发送的消息长度大于msgsz指示的长度,则做截断处理,截断部分将丢弃,且不通知发送消息的进程。

设置消息队列属性

msgctl()函数用于设置或获取共享内存的属性,其函数原型如下:

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>

int msgctl(int msqid, int cmd, struct msqid_ds* buf);
  • msqid:消息队列标识,即msgget()函数的返回值;
  • cmd:对消息队列的操作方式;
  • buf:存放对消息队列操作的信息。

返回值:成功返回0,失败返回-1。

cmd可以是如下取值:

  1. IPC_RMID:删除消息队列;
  2. IPC_STAT:获取消息队列的状态;
  3. IPC_SET:改变消息队列的存取权限。

struct msqid_ds结构体定义如下:

struct msqid_ds {
    struct ipc_perm msg_perm;  // 存取权限
    struct msg* msg_first;      // 指向消息队列头的指针
    struct msg* msg_last;       // 指向消息队列尾的指针
    __kernel_time_t msg_stime;  // 消息队列最后发送时间
    __kernel_time_t msg_rtime;  // 消息队列最后接收时间
    __kernel_time_t msg_ctime;  // 消息队列最后修改时间
    unsigned short msg_cbytes;  // 当前消息队列的字节数
    unsigned short msg_qnum;    // 消息队列中的消息数
    unsigned short msg_qbytes;  // 消息队列的最大字节数
    __kernel_ipc_pid_t msg_lspid;  // 最后发送消息的进程ID
    __kernel_ipc_pid_t msg_lrpid;  // 最近接收消息的进程ID
};

其中struct ipc_perm结构体用于存放IPC对象标识和创建者的信息,定义如下:

struct ipc_perm {
    ket_t key;   // IPC对象标识
    uid_t uid;   // 用户ID
    gid_t gid;   // 用户组ID
    uid_t cuid;  // 创建用户ID
    gid_t cgid;  // 创建用户组ID
    unsigned short mode;  // 存取权限
    unsigned short seq;   // 序列号
};

共享内存(Shared Memory)

共享内存是内核中的一块存储空间,这块内存被映射至多个进程的虚拟地址空间。共享内存在不同进程虚拟地址空间中的映射地址未必相同。通过这块共享内存,不同进程之间可以交换数据,并且对共享内存内容的改变,也会立即在进程的虚拟地址空间中得到反映

基于共享内存的进程通信示意图.png

创建共享内存

shmget()函数用于创建一块共享内存,其函数原型如下:

#include <sys/ipc.h>
#include <sys/shm.h>

int shmget(key_t key, int size, int shmflg);
  • key:用于区分不同共享内存段的参数(非0整数);
  • size:共享内存大小;
  • sgmflg:存取权限或创建条件。

返回值:成功返回一个与key相关的共享内存标识符(非负整数),用于后续的共享内存函数;失败返回-1。

如果key为IPC_PRIVATE,则内核创建一块新的共享内存;否则,如果key所对应的共享内存已存在,则返回该共享内存。shmflg用二进制来标识,其最小9位用于说明共享内存的存取权限,如果shmflg包含IPC_CREAT,则表示创建共享内存。

实际应用中,key可以通过ftok()函数来生成,其函数原型如下:

#include <sys/types.h>
#include <sys/ipc.h>

key_t ftok(const char *pathname, int proj_id);
  • pathname:文件或目录的路径,如果是文件,此文件必须存在且可存取;
  • proj_id:根据自己约定可随意设置的一个序号,取值范围为[1,255]之间的整数。

返回值:成功返回key值(非0整数),否则返回-1。

ftok()函数的实质是把从pathname导出的信息与proj_id的低序8位组合成一个key,由于每个文件在Linux系统中的inode具有唯一性,所以可以使用pathname来生成key值。

共享内存映射的建立

shmat()函数用于将创建的共享内存映射至虚拟地址空间中的某个区间,其函数原型如下:

#include <sys/types.h>
#include <sys/shm.h>

void* shmat(int shmid, const void* shmaddr, int shmflg);
  • shmid:共享内存标识,即shmget()函数的返回值;
  • shmaddr:进程虚拟地址,通常为NULL,表示由系统决定对应的地址;
  • shmflg:读写标志,如果指定了SHM_RDONLY位,则以只读的方式使用该共享内存区域,否则以读写的方式使用。

返回值:成功返回共享内存所映射的虚拟地址空间中的地址,否则返回-1。

共享内存映射的解除

shmdt()函数用于解除已有共享内存的映射关系,其函数原型如下:

#include <sys/types.h>
#include <sys/shm.h>

int shmdt(const void* shmaddr);
  • shmaddr:共享内存首地址。

返回值:成功返回0,否则返回-1。

设置共享内存属性

shmctl()函数用于设置或获取共享内存的属性,其函数原型如下:

#include <sys/ipc.h>
#include <sys/shm.h>

int shmctl(int shmid, int cmd, struct shmid_ds* buf);
  • shmid:共享内存标识,即shmget()函数的返回值;
  • cmd:对共享内存的操作方式;
  • buf:存放对共享内存操作的信息。

返回值:成功返回0,失败返回-1。

cmd可以是如下取值:

  1. IPC_STAT:获取共享内存的状态;
  2. IPC_SET:设置共享内存的权限;
  3. IPC_RMID:删除共享内存;
  4. IPC_LOCK:锁定共享内存,使之不被置换;
  5. IPC_UNLOCK:解锁共享内存。

struct shmid_ds结构体定义如下:

struct shmid_ds {
    struct ipc_perm shm_perm;  // 存取权限
    int    shm_segsz;          // 共享内存大小
    __kernel_time_t shm_atime;  // 共享内存最后映射时间
    __kernel_time_t shm_dtime;  // 共享内存最后解除映射时间
    __kernel_time_t shm_ctime;  // 共享内存最后修改时间
    __kernel_ipc_pid_t shm_cpid;  // 创建进程ID
    __kernel_ipc_pid_t shm_lpid;  // 最近操作进程ID
    unsigned short shm_nattch;    // 建立映射的进程数
};
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,445评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,889评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,047评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,760评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,745评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,638评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,011评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,669评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,923评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,655评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,740评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,406评论 4 320
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,995评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,961评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,023评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,483评论 2 342

推荐阅读更多精彩内容