半双工管道、全双工管道、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的用途:
- shell 命令使用 FIFO 将数据从一条管道传送到另一条时,无需创建中间临时文件。
- 客户进程-服务器进程应用程序中,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 信号量接口的几个缺陷:
- 更高性能的实现
- 没有信号量集,更加简单
- 操作能继续正常工作直到该信号量的最后一次引用被释放;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 函数可以用来检索信号量值