管道和FIFO仍能有效引用于大量应用程序,但是在新程序中要避免使用消息队列和信号量,而应当考虑使用全双工管道和记录锁。
15.1 管道
管道的局限性:
- 半双工
- 只能在具有公共祖先的两个进程之间使用
- 管道是通过调用pipe函数创建的
当读一个写端已被关闭的管道时,在所有数据都被读取后read返回0,表示文件结束。
如果写一个读端已被关闭的管道,则产生信号SIGPIPE。
例子1
使用管道传递数据
#include "apue.h"
int main(void) {
int n;
int fd[2];
pid_t pid;
char line[MAXLINE];
//传入两个fd, fd[0]作为读,fd[1]作为写端
if (pipe(fd) < 0)
err_sys("pipe error");
if ((pid = fork()) < 0)
err_sys("fork error");
else if (pid > 0) {
close(fd[0]);
write(fd[1], "hello, world\n", 12); //父进程写管道,子进程接收
} else {
close(fd[1]);
n = read(fd[0], line, MAXLINE);
write(STDOUT_FILENO, line, n);
}
exit(0);
}
例子2
使用管道进程父子进程同步
#include "apue.h"
static int pfd1[2], pfd2[2];
//父进程 写端->读端
//子进程 写端->读端
void TELL_WAIT(void)
{
if (pipe(pfd1) < 0 || pipe(pfd2) < 0)
err_sys("pipe error");
}
void TELL_PARENT(pid_t pid)
{
if (write(pfd2[1], "c", 1) != 1) //向子进程的写端写数据
err_sys("write error");
}
void WAIT_PARENT(void)
{
char c;
if (read(pfd1[0], &c, 1) != 1) //从父进程的读端读数据
err_sys("read error");
if (c != 'p')
err_quit("WAIT_PARENT: incorrect data");
}
void WAIT_CHILD(void)
{
char c;
if (read(pfd2[0], &c, 1) != 1) //从子进程的读端读数据,读不到阻塞
err_sys("read error");
if (c != 'p')
err_quit("WAIT_PARENT: incorrect data");
}
void TELL_CHILD(pid_t pid)
{
if (write(pfd1[1], "c", 1) != 1) //向父进程的写端写数据
err_sys("write error");
}
例子3
分页展示数据
#include "apue.h"
#include <sys/wait.h>
#define DEF_PAGER "/usr/bin/more"
int main(int argc, char *argv[]) {
int n;
int fd[2];
pid_t pid;
char *pager, *argv0;
char line[MAXLINE];
FILE *fp;
if (argc != 2)
err_quit("usage: pager <pathname>");
if ((fp = fopen(argv[1], "r")) == NULL)
err_sys("can't open %s", argv[1]);
if (pipe(fd) < 0)
err_sys("pipe error");
if ((pid = fork()) < 0) {
err_sys("fork error");
} else if (pid > 0) {
close(fd[0]);
while (fgets(line, MAXLINE, fp) != NULL) { //父进程不断读取数据
n = strlen(line);
if (write(fd[1], line, n) != n) //写数据到写端,子进程会从读端读到
err_sys("write error to pipe");
}
if (ferror(fp))
err_sys("fgets error");
close(fd[1]); //关闭写端,子进程会关闭读端
if (waitpid(pid, NULL, 0) < 0) //获取子进程退出状态
err_sys("waitpid error");
exit(0);
} else {
close(fd[1]); //子进程关闭写端
if (fd[0] != STDIN_FILENO) { //如果读端不等于标准输入
if (dup2(fd[0], STDIN_FILENO) != STDIN_FILENO) //使标准输入成为管道的读端
err_sys("dup2 error to stdin");
close(fd[0]); //原来的fd不再需要
}
if ((pager = getenv("PAGER")) == NULL)
pager = DEF_PAGER;
if ((argv0 = strrchr(pager, '/')) != NULL)
argv0++;
else
argv0 = pager;
//启动分页程序,能从标准输入获取到父进程传来的数据
if (execl(pager, argv0, (char*)0) < 0)
err_sys("execl error for %s", pager);
}
exit(0);
}
15.2 popen和pclose
这两个函数实现的操作是:创建一个管道,fork一个子进程,关闭未使用的管道端,执行一个shell运行命令,然后等待命令终止。
popen先执行fork,然后调用exec执行cmdstring,并且返回一个标准I/O文件指针。
pclose函数关闭标准I/O流,等待命令终止,然后返回shell的终止状态。
例子
分页展示管道内容,使用more命令。父进程从文件读取数据,写到管道,子进程more从管道读取数据,输出到控制台。
#include "apue.h"
#define DEF_PAGER "${PAGER:-more}"
int main(int argc, char *argv[]) {
char line[MAXLINE];
FILE *fpin, *fpout;
if (argc != 2)
err_quit("usage: pager <pathname>");
if ((fpin = fopen(argv[1], "r")) == NULL)
err_sys("popen error");
if ((fpout = popen(DEF_PAGER, "w")) == NULL) //以写的方式打开管道
err_sys("popen error");
while (fgets(line, MAXLINE, fpin) != NULL) { //父进程不断读取数据
if (fputs(line, fpout) == EOF)
err_sys("fputs error to pipe");
}
if (ferror(fpin))
err_sys("fgets error");
if (pclose(fpout) == -1)
err_sys("pclose error");
exit(0);
}
使用管道在标准输入输出中间添加自定义处理程序。
#include "apue.h"
int main(void) {
char buf[MAXLINE];
FILE *fpin;
if ((fpin = popen("./myuclc", "r")) == NULL) //写读打开管道
err_sys("popen error");
for(;;) {
fputs("prompt> ", stdout);
fflush(stdout);
if ((fgets(buf, MAXLINE, fpin)) == NULL) //从管道读取数据
break;
if (fputs(buf, stdout) == EOF)
err_sys("fputs error to pipe");
}
if (pclose(fpin) == -1)
err_sys("pclose error");
putchar('\n');
exit(0);
}
myuclc执行的工作是从标准输入读取字符转换为小写。数据从标准输入到子进程,转换成小写之后,从子进程输出到管道,父进程从管道读取到buf,输出到标准输出。
15.3 协同进程
popen只提供连接到另一个进程的标准输入或标准输出的一个单向管道。
当一个过滤程序既产生某个过滤程序的输入,又读取该过滤程序的输出时,就变成了协同进程(coprocess)。
例子
#include "apue.h"
static void sig_pipe(int);
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){
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, n)) < 0) //从管道读
err_sys("read error");
if (n == 0) { //没有读到数据,管道已关闭
err_msg("child close pipe");
break;
}
line[n] = 0;
if (fputs(line, stdout) == EOF) //输出
err_sys("fputs error");
}
if (ferror(stdin))
err_sys("fgets error on stdin");
exit(0);
} else {
close(fd1[1]);
close(fd2[0]);
if (fd1[0] != STDIN_FILENO) { //保证对应的输入输出是标准输入和标准输出
if (dup2(fd1[0], STDIN_FILENO) != STDIN_FILENO) //dup2 重定向标准输入输出到管道
err_sys("dup2 error");
close(fd1[0]);
}
if (fd2[1] != STDOUT_FILENO) {
if (dup2(fd2[1], STDOUT_FILENO) != STDOUT_FILENO)
err_sys("dup2 error");
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);
}
如果在等待读取时add2进程被杀死,则会产生SIGPIPE信号。
add2用来读取输入的两个整数,返回相加的结果。
#include "apue.h"
int main(void) {
int n, int1, int2;
char line[MAXLINE];
while ((n = read(STDIN_FILENO, line, MAXLINE)) > 0) { //这里的读和写都是非缓冲区的,否则无法正常工作。
line[n] = 0;
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");
}
}
}
15.4 FIFO
FIFO有时被称为命名管道,通过FIFO不相关的进程也能交换数据。
若写一个尚无进程为读而打开的 FIFO,则产生信号SIGPIPE。
若某个FIFO的最后一个写进程关闭了该FIFO,则将为该FIFO的读进程产生一个文件结束标志。
FIFO的两个用途:
- 复制sell命令中的输出流,不需要创建中间临时文件。
- 在客户进程和服务器进程二者之间传递数据。
例子1
mkfifo fifo1
prog3 < fifo1 &
prog1 < infile | tee fifo1 | prog2
tee的输出数据被复制了一份。数据流向为
infile->prog1->tee->fifo1->prog3
|->prog2
15.5 XIS IPC
XSI(X/Open System Interface) IPC包括三种,消息队列,信号量及共享存储器。
标识符
每个内核中的 IPC 结构(消息队列、 信号量或共享存储段)都用一个非负整数的标识符加以引用。
标识符是IPC对象的内部名;每个IPC对象都与一个键相关联,将这个键作为该对象的外部名。
XSI IPC为每一个IPC结构关联了一个ipc_perm结构,该结构规定了权限和所有者。
IPC的问题
IPC结构是在系统范围内起作用的,没有引用计数。如果创建消息队列的进程退出,该消息队列及其内容不会被删除。
这些IPC结构在文件系统中没有名字,无法使用针对文件系统的操作,所以需要单独的系统调用。
15.6 消息队列
消息队列是消息的链接表,存储在内核中,由消息队列标识符标识。消息队列最初是为了提高速度的,和现在的IPC相比已经效果有限。考虑到IPC存在的问题,不建议使用。
相关操作
msgget 用于创建一个新队列或打开一个现有队列。
msgsnd 将新消息添加到队列尾端。
msgsnd 把消息添加到队列,msgrcv 用于从队列中取消息。不一定要以先进先出次序取消息,也可以按消息的类型字段取消息。
15.7 信号量
信号量是一个计数器,用于为多个进程提供对共享数据对象的访问。
使用信号量访问共享数据的过程
1. 测试控制该资源的信号量。
2. 若此信号量的值为正,则进程可以使用该资源。在这种情况下进程会将信号量值减1,表示它使用了一个资源单位。
3. 否则若此信号量的值为0,则进程进入休眠状态,直至信号量值大于0。 进程被唤醒后,它返回至步骤1。
信号量的使用接口比较复杂,因为信号量必需定义为含有一个或多个信号量值的集合,不能原子地创建一个信号量集,并且需要考虑IPC存在的问题。
信号量与互斥量,记录锁对比
互斥量,把进程共享互斥量属性设置为PTHREAD_PROCESS_SHARED。当持有互斥量的进程终止时,需要解决互斥量状态恢复的问题。
记录锁,则先创建一个空文件,并且用该文件的第一个字节(无需存在)作为锁字节。当一个进程终止时, 它所建立的锁全部释放。
信号量,先创建信号量集合并初始化。对每个操作都指定SEM_UNDO,由内核处理在未释放资源条件下进程终止的情况。
结论
在Linux上,记录锁比信号量快,但是共享存储中的互斥量的性能比信号量和记录锁的都要优越。
记录锁使用比信号量简单,当进程终止时系统会管理遗留下来的锁,而共享存储中的互斥量下进程恢复很难处理。
15.8 共享存储
共享存储允许两个或多个进程共享一个给定的存储区。这是最快的一种 IPC,但是需要在多个进程之间同步访问一个给定的存储区。
XSI共享存储和mmap内存映射不同之处在于,前者没有相关的文件,XSI共享存储段是内存的匿名段。
例子,打印不同类型存储地址
#include "apue.h"
#include <sys/shm.h>
#define ARRAY_SIZE 40000
#define MALLOC_SIZE 100000
#define SHM_SIZE 100000
#define SHM_MODE 0600
char array[ARRAY_SIZE]; //全局变量,堆空间
int main(void) {
int shmid; //自动变量在栈上
char *ptr, *shmptr;
printf("array[] from %p to %p\n", (void*)&array[0], (void *)&array[ARRAY_SIZE]);
printf("stack around %p\n", (void *)&shmid);
if ((ptr = malloc(MALLOC_SIZE)) == NULL) //在堆空间分配
err_sys("malloc error");
printf("malloced from %p to %p\n", (void *)ptr, (void *)ptr + MALLOC_SIZE); //void * 转成字节指针
if ((shmid = shmget(IPC_PRIVATE, SHM_SIZE, SHM_MODE)) < 0) //共享存储,在堆和栈之间,未定义段
err_sys("shmget error");
if ((shmptr = shmat(shmid, 0, 0)) == (void *)-1)
err_sys("shmat error");
printf("shared memory from %p to %p\n", (void *)shmptr, (void *)shmptr + SHM_SIZE);
if (shmctl(shmid, IPC_RMID, 0) < 0)
err_sys("shmctl error");
exit(0);
}
结果
array[] from 0x10309b0a0 to 0x1030a4ce0
stack around 0x7ffeecb6c7b8
malloced from 0x7f81fa500000 to 0x7f81fa5186a0
shared memory from 0x1030c4000 to 0x1030dc6a0
回忆7.5中的存储空间结构
高地址
----------------------
命令行参数和环境变量
栈 -->shmid;
|
共享存储 --> shmget
|
堆 ---> malloc
未初始化数据段(bss)--> array[]
初始化数据段
正文
------------------------
低地址
存储映射的对比
可以使用/dev/zero作为设备,调用mmap创建映射区。这种方式同样无需存在一个实际文件,但是只在两个相关进程之间起作用。
在两个无关进程之间要使用共享存储段,一是应用程序使用XSI共享存储函数,或者使用mmap将同一文件映射至它们的地址空间,并使用MAP_SHARED标志。
15.9 POSIX信号量
POSIX信号量接口意在解决XSI信号量接口的缺陷。
- 考虑到了更高性能的实现
- 接口使用更简单
- 在删除时表现更完美
POSIX信号量有命名的和未命名的。前者只能被在同一进程内线程访问;后者能被其他进程通过名字访问。