第十五章 进程间通信

半双工管道、全双工管道、FIFO

UDS,管道

同一台主机的两个进程之间的 IPC,套接字和 STREAMS 是仅有的支持不同主机上两个进程之间 IPC 的两种形式

本章讨论经典的 IPC:管道、FIFO、消息队列、信号量 以及共享存储

管道

局限性:历史上半双工,数据单向;在公共祖先的两个进程间使用,通常父子进程使用

FIFO没有第二种限制,uds没有这两种限制

每当在管道中键入一个命令序列, 让 shell 执行时,shell 都会为每一条命令单独创建一个进程,然后用管道将前一条命令进程的标 准输出与后一条命令的标准输入相连接

管道是通过调用 pipe 函数创建的

#include <unistd.h>
int pipe(int fd[2]);
fd[0]读 <- fd[1]写
fd[1]的输出是fd[0]的输入

单个进程中的管道几乎没有任何用处

管道连接着一个写端的进程,一个读端的进程;读一个写端的管道,写一个读端的管道

popen和pcloose

创建一个管道,fork 一个子进程,关闭未使用的管道端,执行一个 shell 运行命令,然后等待命令终止。

#include <stdio.h>
FILE *popen(const char *cmdstring, const char *type);
int pclose(FILE *fp);

popen 的r模式: 子进程cmd的标准输出连接到父进程的文件指针

popen 的w模式:父进程的文件指针连接到子进程的cmd的标准输入

协同进程

当一个过滤程序既产生某个过滤程序的输入,又读取该过滤程序的输出时,它就变成了协同进程

两个pipe实现可以双工

FIFO

前面用的其实是未命名管道,只能两个相关进程使用,如父子进程,而FIFO是命名管道,不相关进程也能通信

#include <sys/stat.h>
int mkfifo(const char *path,mode_t mode);
int mkfifoat(int fd,const char *path,mode_t mode);

mode与open的mode一样

mkfifoat 函数可以被用来在 fd 文件描述符表 示的目录相关的位置创建一个 FIFO

FIFO的用途:

  1. shell 命令使用 FIFO 将数据从一条管道传送到另一条时,无需创建中间临时文件。
  2. 客户进程-服务器进程应用程序中,FIFO 用作汇聚点,在客户进程和服务器进程二者之 间传递数据

XSI IPC

有 3 种称作 XSI IPC 的 IPC:消息队列,信号量,共享存储器

XSI IPC 函数是紧密地基于 System V 的 IPC 函数的

标识符和键

与文件描述符不同,IPC 标识符不是小的整数。当一个 IPC 结构被创建,然后又被 删除时,与这种结构相关的标识符连续加 1,直至达到一个整型数的最大正值,然后又回转到 0

有多种方法使客户进程和服务器进程在同一 IPC 结构上汇聚

1,2,3;1,2都有明显缺点,3是基于2,客服2的缺点

#include <sys/ipc.h>
key_t ftok(const char *path, int id);

客户进程和服务器进程认同一个路径名和项目 ID(项目 ID 是 0~255 之 接着,调用函数 ftok 将这两个值变换为一个键。

权限结构

struct ipc_perm{
    uid_t uid;
    gid_t gid;
    uid_t cuid;
    gid_t cgid;
    mode_t mode;
    ...
}
权限
用户读 0400
用户写(更改) 0200
组读 0040
组写(更改) 0020
其他读 0004
其他写(更改) 0002

结构限制

优缺点

管道和FIFO在最后一个引用进程终止后,会删除数据,但是3中IPC不会

这些 IPC 结构在文件系统中没有名字,需要内核增加十几个全新的系统调用,如msgget,semget,shmget

消息队列

每个消息都由 3 部分组成:一个正的长整型类型的字段、一个非负的长度 (nbytes)以及实际数据字节数(对应于长度)。

消息总是放在队列尾端

消息队列是消息的链接表,存储在内核中,由消息队列标识符标识

msgget 用于创建一个新队列或打开一个现有队列

msgsnd 将新消息添加到队列尾端

msgrcv 用于从队列中取消息

#include <sys/msg.h>
int msgget(key_t key,int flag);
msgget((0x123 + 1), IPC_CREAT | 0666)
int msgctl(int msgid,int cmd,struct msgid_ds *buf);
每个队列都有一个msgid_ds
struct msgid_ds{
    struct ipc_perm msg_perm;
    msggnum_t msg_gnum;
    msglen_t msg_qbytes;
    pid_t msg_lspid;
    pid_t msg_lrpid;
    time_t msg_stime;
    time_t msg_rtime;
    time_t msg_ctime;
    ...
}

msgctl 函数对队列执行多种操作(类似的还有semctl,shmctl)

cmd:也可用于信号量和共享存储

IPC_STAT 取此队列的 msqid_ds 结构,并将它存放在 buf 指向的结构中

IPC_SET 将字段 msg_perm.uid、msg_perm.gid、msg_perm.mode 和 msg_qbytes从 buf 指向的结构复制到与这个队列相关的 msqid_ds 结构中

IPC_RMID 从系统中删除该消息队列以及仍在该队列中的所有数据。这种删除立即生效

#include <sys/msg.h>
int msgsnd(int msgid,const void *ptr,size_t nbytes,int flag);
ptr 就是一个指向 mymesg 结构的指针
struct mymesg{
    long mtype;
    char mtext[512];
}

对删除消息队列的处理不是很完善,没有维护引用计数器

ptr 参数指向一个长整型数,它包含了正的整型消息类型,其后紧接着的是消息数据(若 nbytes 是 0,则无消息数据)
参数 flag 的值可以指定为 IPC_NOWAIT。这类似于文件 I/O 的非阻塞 I/O 标志

当 msgsnd 返回成功时,消息队列相关的 msqid_ds 结构会随之更新,表明调用的进程 ID (msg_lspid)、调用的时间(msg_stime)以及队列中新增的消息(msg_qnum)

msgrcv 从队列中取用消息

#include <sys/msg.h>
ssize_t msgrcv(int msqid, void *ptr, size_t nbytes, long type, int flag);

flag 中设置了 MSG_NOERROR 位,则该消息会被截断;flag 值指定为 IPC_NOWAIT,使操作不阻塞

参数 type 可以指定想要哪一种消息

msgrcv 成功执行时,内核会更新与该消息队列相关联的 msgid_ds 结构,以指示调用者的进程 ID(msg_lrpid)和调用时间(msg_rtime),并指示队列中的消息数减少了 1 个(msg_qnum)

消息队列与全双工管道的时间比较

客户进程和服务器进程之间的双向数据流:

可以使用消息队列或全双工管道

可以使全双工管道可用,而某些平台通过 pipe 函数提供全双工管道

考虑到使用消息队列时遇到的问题(见 15.6.4 节),我们得出的 结论是,在新的应用程序中不应当再使用它们

信号量

信号量与已经介绍过的 IPC 机构(管道、FIFO 以及消息列队)不同。它是一个计数器,用 于为多个进程提供对共享数据对象的访问。

若此信号量的值为正,则进程可以使用该资源。在这种情况下,进程会将信号量值减 1, 表示它使用了一个资源单位

否则,若此信号量的值为 0,则进程进入休眠状态,直至信号量值大于 0。进程被唤醒 后

当进程不再使用由一个信号量控制的共享资源时,该信号量值增 1。如果有进程正在休眠等 待此信号量,则唤醒它们

内核为每个信号量集合维护着一个 semid_ds 结构
struct semid_ds{
    struct ipc_perm sem_perm;
    unsigned short sem_nsems;
    time_t sem_otime;
    time_t sem_ctime;
    ...
}
每个信号量由一个无名结构表示
struct{
    unsigned short semval;
    pid_t ssempid;
    unsigned short semncnt;
    unsigned short semzcnt;
    ...
}

调用函数 semget 来获得一个信号量 ID

#include <sys/sem.h>
int semget(key_t key, int nsems, int flag);
int semctl(int semid, int semnum, int cmd, ... /* union semun arg */);

初始化 ipc_perm 结构。该结构中的 mode 成员被设置为 flag 中的 相应权限位

nsems 是该集合中的信号量数。如果是创建新集合(一般在服务器进程中),则必须指定 nsems。 如果是引用现有集合(一个客户进程),则将 nsems 指定为 0。

cmd:10种

函数 semop 自动执行信号量集合上的操作数组

#include <sys/sem.h>
int semop(int semid, struct sembuf semoparray[], size_t nops);

参数 semoparray 是一个指针,它指向一个由 sembuf 结构表示的信号量操作数组

参数 nops 规定该数组中操作的数量(元素数)。

struct sembuf{
    unsigned short sem_num;
    short sem_op;//对集合中每个成员的操作由相应的 sem_op 值规定
    short sem_flg;//IPC_NOWAIT, SEM_UNDO
}

sem_op 为正值,这对应于进程释放的占用的资源数,需要加到信号量的值上;如果指定了 undo 标志,则也从该进程的此信号量调整值中减去 sem_op

sem_op 为负值,信号量的值大于等于 sem_op 的绝对值,从信号量值中减去 sem_op 的绝对值;如果指定了 undo 标志,则 sem_op 的绝对值也 加到该进程的此信号量调整值上

正负两者操作其实是一个道理

sem_op 为 0,这表示调用进程希望等待到该信号量值变成 0

exit 时的信号量调整

正如前面提到的,如果在进程终止时,它占用了经由信号量分配的资源,那么就会成为一个 问题。

信号量、记录锁和互斥量的时间比较

共享存储

信号量用于同步共享存储访问

在多个进程将同一个文件映射到它们的地址空间 的时候。

XSI 共享存储和内存映射的文件的不同之处在于,前者没有相关的文件。

XSI 共享存储 段是内存的匿名段

内核为每个共享存储段维护着一个结构

struct shmid_ds{
    struct ipc_perm shm_perm;
    size_t shm_segsz;
    pid_t shm_lpid;
    pid_t shm_cpid;
    shmatt_t shm_nattch;
    time_t shm_atime;
    time_t shm_dtime;
    time_t shm_ctime;
    ...
}

调用的第一个函数通常是 shmget,它获得一个共享存储标识符

#include <sys/shm.h>
int shmget(key_t key, size_t size, int flag);

mode 按 flag 中的相应权限位 设置

shmctl 函数对共享存储段执行多种操作

#include <sys/shm.h>
int shmctl(int shmid, int cmd, struct shmid_ds *buf);

Cmd:5种命令

一旦创建了一个共享存储段,进程就可调用 shmat 将其连接到它的地址空间中

#include <sys/shm.h>
void *shmat(int shmid, const void *addr, int flag);
int shmdt(const void *addr);

如果 addr 为 0,则此段连接到由内核选择的第一个可用地址上。这是推荐的使用方式

flag 中指定了 SHM_RDONLY 位,则以只读方式连接此段,否则以读写方式连接此段

shmat 的返回值是该段所连接的实际地址,如果出错则返回−1;成功,内核将使与该共享存储段相关的 shmid_ds 结构中的 shm_nattch 计数器值加 1

共享存储段紧靠在 栈之下

POSIX信号量

POSIX 信号量接口意在解决 XSI 信号量接口的几个缺陷:

  1. 更高性能的实现
  2. 没有信号量集,更加简单
  3. 操作能继续正常工作直到该信号量的最后一次引用被释放;XSI 信号量被删除时,使用这个 信号量标识符的操作会失败

POSIX 信号量有两种形式:命名的和未命名的

调用 sem_open 函数来创建一个新的命名信号量或者使用一个现有信号量

#include <semaphore.h>
sem_t *sem_open(const char *name, int oflag, ... /* mode_t mode,unsigned int value */ );
int sem_close(sem_t *sem);

当使用一个现有的命名信号量时,我们仅仅指定两个参数:信号量的名字和 oflag 参数的 0 值
当这个 oflag 参数有 O_CREAT 标志集时,如果命名信号量不存在,则创建一个新的。如果它 已经存在,则会被使用,但是不会有额外的初始化发生

当我们指定 O_CREAT 标志时,需要提供两个额外的参数。mode 参数指定谁可以访问信号量。 mode 的取值和打开文件的权限位相同

在创建信号量时,value 参数用来指定信号量的初始值。它的取值是 0~SEM_VALUE_MAX

如果我们想确保创建的是信号量,可以设置 oflag 参数为 O_CREAT|O_EXCL。如果信号量已 经存在,会导致 sem_open 失败

可以使用 sem_unlink 函数来销毁一个命名信号量

#include <semaphore.h>
int sem_unlink(const char *name);

sem_unlink 函数删除信号量的名字。如果没有打开的信号量引用,则该信号量会被销毁。 否则,销毁将延迟到最后一个打开的引用关闭

可以使用 sem_wait 或者 sem_trywait 函数来实现信号量的减 1 操作

#include <semaphore.h>
int sem_trywait(sem_t *sem);
int sem_wait(sem_t *sem);
int sem_timedwait(sem_t *restrict sem,const struct timespec *restrict tsptr);

使用 sem_wait 函数时,如果信号量计数是 0 就会发生阻塞。直到成功使信号量减 1 或者被 信号中断时才返回

可以调用 sem_post 函数使信号量值增 1

#include <semaphore.h>
int sem_post(sem_t *sem);

当我们想在单个进程中使用 POSIX 信号量时,使用未命名信号量更容易。这仅仅改变创建和 销毁信号量的方式。可以调用 sem_init 函数来创建一个未命名的信号量。

#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);
int sem_destroy(sem_t *sem);
int sem_getvalue(sem_t *restrict sem, int *restrict valp);

pshared 参数表明是否在多个进程中使用信号量。如果是,将其设置成一个非 0 值。value 参 数指定了信号量的初始值。

如果要在两个进程之间使用信号量,需要确保 sem 参数指向两个进程之间共享的内存范围

对未命名信号量的使用已经完成时,可以调用 sem_destroy 函数丢弃它

调用 sem_destroy 后,不能再使用任何带有 sem 的信号量函数,除非通过调用 sem_init 重新初始化它

sem_getvalue 函数可以用来检索信号量值

客户进程-服务器进程属性

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,108评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,699评论 1 296
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,812评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,236评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,583评论 3 288
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,739评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,957评论 2 315
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,704评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,447评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,643评论 2 249
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,133评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,486评论 3 256
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,151评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,108评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,889评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,782评论 2 277
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,681评论 2 272

推荐阅读更多精彩内容