本章讨论经典的IPC:管道、FIFO、消息队列、信号量以及共享存储器
管道是Unix系统IPC最古老的方式。管道有下列两种局限性:
(1) 历史上,它们是半双工的(即数据只能在一个方向上流动)。
(2) 它们只能在具有公共祖先的进程之间使用。通常,一个管道由一个进程创建,然后该进程调用fork,此后父子进程就可以应用该管道
#include <unistd.h>
int pipe(int filedes[2]);
Returns: 0 if OK, 1 on error
经由参数filedes返回两个文件描述符:filedes[0]为读而打开,filedes[1]为写而打开。Filedes[1]的输出是filedes[0]的输入
经由管道从父进程向子进程传递数据
#include "apue.h"
Int main(void)
{
int n;
int fd[2];
pid_t pid;
char line[MAXLINE];
if (pipe(fd) < 0)
err_sys("pipe error");
if ((pid = fork()) < 0) {
err_sys("fork error");
} else if (pid > 0) { /* parent */
close(fd[0]);
write(fd[1], "hello world\n", 12);
} else { /* child */
close(fd[1]);
n = read(fd[0], line, MAXLINE);
write(STDOUT_FILENO, line, n);
}
exit(0);
}
在管道操作中,常见的操作是创建一个连接到另一个进程的管道,然后读其输出或向其发送输入,所以标准I/O库为实现这些操作提供了两个函数POPEN和PCLOSE,这两个函数实现的操作是::
1创建一个管道
2FORK 一个子进程
3关闭管道的不是用端
4EXEC一个SHELL以执行命令
5等待命令终止
#include <stdio.h>
FILE *popen(const char *cmdstring, const char *type);
Returns: file pointer if OK, NULL on error
int pclose(FILE *fp);
Returns: termination status of cmdstring, or 1 on error
当一个程序产生某个过滤程序的输入,同时又读取该过滤程序的输出时,则该过滤程序就成为协同进程。
#include "apue.h"
Int main(void)
{
int n, int1, int2;
char line[MAXLINE];
while ((n = read(STDIN_FILENO, line, MAXLINE)) > 0) {
line[n] = 0; /* null terminate */
if (sscanf(line, "%d%d", &int1, &int2) == 2) {
sprintf(line, "%d\n", int1 + int2);
n = strlen(line);
if (write(STDOUT_FILENO, line, n) != n)
err_sys("write error");
} else {
if (write(STDOUT_FILENO, "invalid args\n", 13) != 13)
err_sys("write error");
}
}
exit(0);
}
将此程序编译,把可执行目标代码存入名为add2的文件
使用add2的程序
#include "apue.h"
static void sig_pipe(int); /* our signal handler */
Int main(void)
{
int n, fd1[2], fd2[2];
pid_t pid;
char line[MAXLINE];
if (signal(SIGPIPE, sig_pipe) == SIG_ERR)
err_sys("signal error");
if (pipe(fd1) < 0 || pipe(fd2) < 0)
err_sys("pipe error");
if ((pid = fork()) < 0) {
err_sys("fork error");
} else if (pid > 0) { /* parent */
close(fd1[0]);
close(fd2[1]);
while (fgets(line, MAXLINE, stdin) != NULL) {
n = strlen(line);
if (write(fd1[1], line, n) != n)
err_sys("write error to pipe");
if ((n = read(fd2[0], line, MAXLINE)) < 0)
err_sys("read error from pipe");
if (n == 0) {
err_msg("child closed pipe");
break;
}
line[n] = 0; /* null terminate */
if (fputs(line, stdout) == EOF)
err_sys("fputs error");
}
if (ferror(stdin))
err_sys("fgets error on stdin");
exit(0);
} else { /* child */
close(fd1[1]);
close(fd2[0]);
if (fd1[0] != STDIN_FILENO) {
if (dup2(fd1[0], STDIN_FILENO) != STDIN_FILENO)
err_sys("dup2 error to stdin");
close(fd1[0]);
}
if (fd2[1] != STDOUT_FILENO) {
if (dup2(fd2[1], STDOUT_FILENO) != STDOUT_FILENO)
err_sys("dup2 error to stdout");
close(fd2[1]);
}
if (execl("./add2", "add2", (char *)0) < 0)
err_sys("execl error");
}
exit(0);
}
static void sig_pipe(int signo)
{
printf("SIGPIPE caught\n");
exit(1);
}
在程序中创建两个管道,父子进程各自关闭它们不需要的端口。两个管道一个用作协同进程的标准输入,另一个做标准输出。子进程调用dup2使管道描述符移至其标准输入和输出,然后调用execl
创建FIFO类似于创建文件。确实,FIFO的路径名存在于文件系统中。
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
Returns: 0 if OK, 1 on error
Mkfifo函数中的mode参数的规格说明与open函数中的mode相同。一旦用mkfifo创建了FIFO,就可以用open打开。其实,一般的文件IO都可以用于FIFO
FIFO有两种用途
(1)由 shell 命令使用以便将数据从一条管道线传送到另一条,为此无需创建临时文件
(2)用于 client-server 进程应用程序中,以在 client 和 server 间传递数据
每个队列的struct msqid_ds结构如下:
struct msqid_ds {
struct ipc_perm msg_perm; /* see Section 15.6.2 */
msgqnum_t msg_qnum; /* # of messages on queue */
msglen_t msg_qbytes; /* max # of bytes on queue */
pid_t msg_lspid; /* pid of last msgsnd() */
pid_t msg_lrpid; /* pid of last msgrcv() */
time_t msg_stime; /* last-msgsnd() time */
time_t msg_rtime; /* last-msgrcv() time */
time_t msg_ctime; /* last-change time */
.
.
.
};
打开一个现存的队列或者创建一个新队列–msgget
#include <sys/msg.h>
int msgget(key_t key, int flag);
Returns: message queue ID if OK, 1 on error
队列的设定 – msgctl
msgctl 函数可以执行多种操作,与 ioctl、semctl、shmctl 非常类似,都被称为“垃圾桶函数
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
调用成功返回 0,否则返回 -1
cmd 参数说明
IPC_STAT 取出该队列描述结构,并存放在 buf 指向的结构中
IPC_SET 按buf指向的结构中的值,设置队列描述结构中的msg_perm.uid、msg_perm.gid、msg_perm.mode和msg_qbytes,但是要求执行该函数进程的有效用户ID必须等于msg_perm.cuid或msg_perm.uid,或该进程的有效用户是超级用户,并且只有超级用户才能增加msg_qbytes的值
IPC_RMID 从系统中删除该消息队列及队列中的所有数据,该操作会立即生效,取数据的进程接下来会立即返回EIDRM,与IPC_SET一样,该操作要求执行该函数进程的有效用户ID必须等于msg_perm.cuid或msg_perm.uid,活该进程的有效用户是超级用户
将数据加入队列 – msgsnd
int msgsnd(int msqid, const void *ptr, size_t nbytes, int flag);
调用成功返回 0,否则返回 -1
从队列中取出消息 – msgrcv
#include <sys/msg.h>
ssize_t msgrcv(int msqid, void *ptr, size_t nbytes, long type, int flag);
Returns: size of data portion of message if OK, 1 on error
参数说明
ptr—返回的消息存储在 ptr 指向的缓冲区中,缓冲区开始部分的长整型标识实际数据的大小
type—
type == 0 返回队列中的第一个消息(先进先出)
type > 0 返回队列中消息类型为 type 的消息
type < 0 返回队列中消息类型值不大于 type 绝对值的消息中类型值最小的一个消息
flag—
MSG_NOERROR 如果消息大于缓冲区,则被截断并正确返回,如果没有设置,则函数返回 E2BIG 错误信息
IPC_NOWAIT 操作不阻塞,如果队列为空则直接返回-1,errno设置为ENOMSG,如果没有设置IPC_NOWAIT则等待直到队列中有数据写入(如果在等待过程中队列被删除,则返回EINTR)
信号量与其他的 IPC (管道、FIFO、消息队列、域套接字)都有所不同,他是一个计数器,用于多进程对共享数据对象的访问
当进程需要获得共享资源时,需要进行下列操作:
当然了,对信号量的测试及减 1 的操作必须是原子操作,因此,通常信号量是内核实现的
最常用的信号量初始值为 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 sempid; // 最后操作该信号量的进程 ID
unsigned short semncnt; // 因为信号量值过大而等待的进程数
unsigned short semzcnt; // 因为信号量值等于 0 而等待的进程数
...
}
信号量的创建与获取 – semget
int semget(key_t key, int nsems, int flag);
调用成功返回信号量集 ID,否则返回 -1
该函数将 key 变换为信号量集标识符,并返回,与消息队列创建函数 msgget 一样:
如果 key 取值为 IPC_PRIVATE,则创建新的 IPC 结构
如果指定的 key 当前未与任何 IPC 结构结合,并且 flag 中指定了 IPC_CREAT 位,则用该 key 创建新的 IPC 结构
如果指定的 key 当前未与任何 IPC 结构结合,并且 flag 中未指定 IPC_CREAT 位,则函数返回出错
如果指定的 key 当前已经与 IPC 结构结合,并且 flag 中未指定 IPC_EXECL 位,则返回对应 IPC 结构,否则返回 EEXIST
参数 nsems 用于初始化该信号量集描述结构的 sem_nsems 字段
信号量集的设定 – semctl
semctl 函数可以执行多种操作,与 ioctl、semctl、shmctl 非常类似,都被称为“垃圾桶函数”
int semctl(int semid, int semnum, int cmd [, semun arg ]);
根据 cmd 参数的不同,返回值有所不同
信号量操作函数 – semop
函数 semop 执行信号量集上的操作,即本文开头处图片中的过程,是一个原子操作
int semop(int semid, struct sembuf semoparray[], size_t nops);
参数说明 – sembuf 结构数组 semoparray
参数 semoparray 是一个信号量操作数组,标识对每个信号量的操作
struct sembuf
{
unsigned short sem_num; // 该操作对应信号量集中信号量编号
short sem_op; // 指定对信号量的操作
int sem_flg; // 信号量操作标志,可选 IPC_NOWAIT、SEM_UNDO
}
共享内存允许两个或更多个进程共享一个给定的存储区域,这是最快的一种 IPC
但是,当某个进程正在读写共享存储的某个区域时,其他进程同时不应该对该区域进行操作,信号量通常被用来实现对共享存储访问的同步,当然,记录所也可以用于这种场合,但是记录锁在时间上要比信号量多消耗约 60%
内核为每个共享存储段设置了一个 shmid_ds 结构
struct shmid_ds
{
struct ipc_perm shm_perm; // 权限结构
size_t shm_segsz; // 存储段大小(字节数)
pid_t shm_lpid; // 最后一个操作共享内存段的进程 ID
pid_t shm_cpid; // 创建共享内存段的进程 ID
shmatt_t shm_nattch; // 连接计数
time_t shm_atime; // 最后执行 attach 操作的时间
time_t shm_dtime; // 最后执行 detach 操作的时间
time_t shm_ctime; // 最后执行 change 操作的时间
...
}
共享存储的创建与获取 – shmget
函数 shmget 用于创建一个新的共享存储段或获取一个已经存在的共享存储段
int shmget(key_t key, size_t size, int flag);
共享存储的设定 – shmctl
shmctl 函数可以执行多种操作,与 ioctl、semctl、shmctl 非常类似,都被称为“垃圾桶函数”
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
cmd 参数可以指定下列 5 种命令的一种
IPC_STAT 获取shmid对应的存储段描述结构shmid_ds并存储在参数buf所指向的内存中
IPC_SET 按buf所指向内存中的shmid_ds结构设置shmid对应的存储段描述结构(可以改变shm_perm.uid、shm_perm.gid、shm_perm.mode),执行此命令的进程有效用户ID必须等于sem_perm.cuid或sem_perm.uid或者该进程具有超级用户权限
IPC_RMID 从系统中删除该共享存储段,与其他两个XSI IPC的相应操作不同,执行后并不会立即删除,除非该存储段的最后一个进程终止或与该段脱离连接(shm_nattch值变为0),执行此命令的进程有效用户ID必须等于sem_perm.cuid或sem_perm.uid或者该进程具有超级用户权限
SHM_LOCK 将共享存储段锁定在内存中,只有超级用户可以执行此命令
SHM_UNLOCK 解锁共享内存段,只有超级用户可以执行此命令
连接共享存储到地址空间 – shmat
一旦创建了一个共享存储段,进程就可以通过调用 shmat 函数将它连接到它的地址空间中:
void *shmat(int shmid, const void *addr, int flag);
addr 参数说明
flag 参数说明
共享存储到地址空间的脱接 – shmdt
当对共享存储段的操作结束时,调用shmdt脱接该段
int shmdt(void *addr);
shmdt 的实际操作是将 shm_nattch 的值减 1