1. 一个简单的客户—服务器例子
- Client从标准输入(stdin)读进一个路径名,并把它写入IPC通道。Server从该IPC通道读出这个路径名,并尝试打开其文件。若server能打开该文件,它能读出其中的内容,并写入(不一定同一个)IPC通道,以作为对客户的响应;否则响应一个错误信息。
2. 管道
管道由pipe函数创建,提供一个单向数据流。
- 该函数返回2个文件描述符:fd[0]和fd[1]。前者读,后者写。【半双工】
- 也可创建【全双工】IPC管道。(全双工:管道两端既可读又可写)
- 【常见用途:shell】
#include <unistd.h>
int pipe(int fd[2]); // 成功则返回0,否则返回-1
单进程中的管道与单进程内的管道
- 单进程中的管道
- 单进程内的管道
管道的典型用途是为两个不同进程(父进程、子进程)提供进程间的通信手段。
1)一个进程(父进程)创建一个管道后调用fork派生一个子进程,接着父进程关闭这个管道的读出端,子进程关闭统一管道的写入端。
3 例子
- main函数创建两个管道并用fork生成一个子进程。客户作为父进程运行,服务器作为子进程运行。第一个管道用于从客户向服务器发送路径名;第二个管道用于从服务器向客户发送该文件的内容。【每个文件描述符只负责读或写】
#include "unpipc.h"
void client(int, int), server(int, int);
void client (int readfd, int writefd) {
size_t len;
ssize_t n;
char buff[MAXLINE];
Fgets(buff, MAXLINE, stdin); // read pathname
len = strlen(buff); // fgets()以空字节作为其结尾
if (buff[len-1] == '\n')
len--; // delete newline from fgets()
Write(writefd, buff, len); // write pathname to IPC channel
while ( (n = Read(readfd, buff, MAXLINE)) > 0)
Write(STDOUT_FILENO, buff, n);
}
void server (int readfd, int writefd) {
int fd;
ssize_t n;
char buff[MAXLINE+1];
// read pathname from IPC channel
if ( (n = Read(readfd, buff, channel)) == 0)
err_quit("end-of-file while reading pathname");
buff[n] = '\0';
// 打开所请求的文件读
// 若出错则返回出错字符串
if ( (fd = open(buff, O_RDONLY)) < 0) {
snprintf(buff + n, sizeof(buff) - n, ": can't open, %s\n", strerror(errno));
n = strlen(buff);
Write(writefd, buff, n);
} else { // 若成功,则将文件内容复制到管道中
// open succeeded: copy file to IPC channel
while ( (n = Read(fd, buff, MAXLINE)) > 0)
Write(writefd, buff, n);
Close(fd);
}
}
int main(int argc, char const *argv[])
{
int pipe1[2], pipe2[2];
pid_t childpid;
Pipe(pipe1); // create 2 pipes
Pipe(pipe2); // create 2 pipes
if ( (childpid = Fork()) == 0) { // child
Close(pipe1[1]); // 子进程关闭pipe1的写端
Close(pipe2[0]); // 子进程关闭pipe2的读端
}
// parent
Close(pipe1[0]); // 父进程关闭pipe1的读端
Close(pipe2[1]); // 父进程关闭pipe2的写端
// 将pipe2[0]写进client,pipe1[0]读client
client(pipe2[0], pipe1[1]);
// 取得已终止子进程的终止状态
Waitpid(childpid, NULL, 0); // wait for child to terminate
exit(0);
return 0;
}
4 全双工管道的真正实现
-
写入fd[1]的数据只能从fd[0]读出,写入fd[0]的数据只能从fd[1]读出。
-
pipe(fd[2]),设置了client / server写入fd[1] / fd[0] 只能从 fd[0] / fd[1]读出。
#include "unpipc.h"
int main(int argc, char const *argv[])
{
int fd[2], n;
char c;
pid_t childpid;
Pipe(fd);
if ( (childpid = Fork()) == 0 ) {
sleep(3);
if ( (n = Read(fd[0], &c, 1)) != 1)
err_quit("child: read returned %d", n);
printf("child read %c\n", c);
Write(fd[0], "c", 1);
exit(0);
}
Write(fd[1], "p", 1);
// 当server 并未往fd[0]写入数据时,此处发生阻塞
if ( (n = Read(fd[1], &c, 1)) != 1)
err_quit("child: read returned %d", n);
printf("parent read %c\n", c);
exit(0);
return 0;
}
5 popen和pclose函数
popen函数创建一个管道并启动另外一个进程,该进程要么从该管道读出标准输入,要么往该管道写入标准输出。
#include <stdio.h>
FILE *popen(const char *command, const char *type); // 成功返回文件指针,否则返回NULL
int pclose(FILE *stream); // 成功返回shell的终止状态,否则返回-1
command是shell命令行,PATH环境变量可用于定位command。popen在调用进程和所指定的命令之间创建一个管道。
- 若type为r,则调用进程读进command的stdout
- 若type为w,则调用进程写进command的stdin
例子:将客户-服务器使用popen()函数实现
#include "unpipc.h"
int main(int argc, char const *argv[])
{
size_t n;
char buff[MAXLINE], command[MAXLINE];
FILE *fp;
Fgets(buff, MAXLINE, stdin);
n = strlen(buff);
if (buff[n-1] == '\n')
n--;
snprintf(command, sizeof(command), "cat %s", buff);
// command内容是路径名
fp = Popen(command,"r");
while (Fgets(buff, MAXLINE, fp) != NULL)
Fputs(buff, stdout);
Pclose(fp);
exit(0);
}`在这里插入代码片`
6 FIFO
管道无名字,只能用于有一个共同祖先进程的各个进程之间,而无法在无亲缘关系的两个进程间创建一个管道并用作IPC通道。
FIFO称为有名管道,由mkfifo函数创建。
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode); // 成功则返回0,否则返回-1
mode参数指定文件权限位,类似于open的第二个参数
例子:使用两个FIFO代替两个管道
这个FIFO例子与之前的管道相比:
- 创建并打开一个管道只需调用pipe。创建并打开一个FIFO则需在调用mkfifo后再调用open;
- 管道在所有进程中最终都关闭它之后自动消失。FIFO的名字则只有通过调用unlink才从文件系统中删除。好处:FIFO在文件系统中有一个名字,该名字允许某个进程创建一个FIFO,与它无亲缘关系的另一个进程打开这个FIFO。
#include "unpipc.h"
// 在/tmp文件系统中创建2个FIFO。这2个FIFO事先存在与否无关紧要
#define FIFO1 "/tmp/fifo.1"
#define FIFO2 "/tmp/fifo.2"
void client(int, int), server(int, int);
int main(int argc, char const *argv[])
{
int readfd, writefd;
pid_t childpid;
// create 2 FIFOs; OK if they already exist
if ( (mkfifo(FIFO1, FILE_MODE) < 0) && (errno != EEXIST))
err_sys("can't create %s", FIFO1);
if ( (mkfifo(FIFO2, FILE_MODE) < 0) && (errno != EEXIST)) {
unlink(FIFO1);
err_sys("can't create %s", FIFO2)
}
if ( (childpid = Fork()) == 0) {
readfd = Open(FIFO1, O_RDONLY, 0);
writefd = Open(FIFO2, O_WRONLY, 0);
server(readfd, writefd);
exit(0);
}
writefd = Open(FIFO1, O_WRONLY, 0);
readfd = Open(FIFO2, O_RDONLY, 0);
client(readfd, writefd);
Waitpid(childpid, NULL, 0); // wait for child to terminate
Close(readfd);
Close(writefd);
Unlink(FIFO1);
Unlink(FIFO2);
exit(0);
}
如果对换父进程中两个open调用的顺序,该程序不工作。其原因在于,若当前尚无任何进程打开某个FIFO写,则打开该FIFO读的进程将阻塞。此时,父子进程都读,二者均阻塞,发生死锁现象。
Note:
- 最后删除所用的FIFO的是client而不是server,因为对这些FIFO执行最终操作的是客户。
- 内核为管道和FIFO维护一个访问计数器,其值是同一个管道或FIFO的打开着的描述符的个数。有了访问计数器后,client或server就能成功地调用unlink。尽管该函数从文件系统中删除了所指定的路径名,先前已经打开该路径名、目前仍打开着的描述符不受影响。
7 管道和FIFO的额外属性
下面是关于管道或FIFO的读出与写入的若干额外规则:
- 若请求写入的数据的字节数小于或等于PIPE_BUF,则write操作保证是原子的。即,若有两个进程差不多同时往管道或FIFO写,则或者先写入来自第一个进程的所有数据,再写入来自第二个进程的所有数据,或者颠倒。 系统不会相互混杂来自这两个进程的数据。然而,若请求写入的数据的字节数大于PIPE_BUF,则write操作不能保证是原子的。
8 单个服务器,多个客户
/tmp/fifo.serv为众所周知的路径名,服务器以此创建一个FIFO,它将从这个FIFO读入客户的请求。每个客户在启动时创建创建自己的FIFO,所用的路径名含有自己的进程ID。每个客户把自己的请求写入服务器的众所周知FIFO中,该请求含有客户的进程ID,以及一个路径名,具有该路径名的文件就是客户希望服务器打开并发回的文件。
8.1 服务器程序
#include "fifo.h"
void server(int, int);
int main(int argc, char const *argv[])
{
int readfifo, writefifo, dummyfd, fd;
char *ptr, buff[MAXLINE+1], fifoname[MAXLINE];
pid_t pid;
ssize_t n;
if ( (mkfifo(SERV_FIFO, FILE_MODE) < 0) && (errno != EEXIST))
err_sys("can't create %s", SERV_FIFO);
readfifo = Open(SERV_FIFO, O_RDONLY, 0);
dummyfd = Open(SERV_FIFO, O_WRONLY, 0); //never used
while ( (n = Readline(readfifo, buff, MAXLINE)) > 0) {
if (buff[n-1] == '\n')
n--;
buff[n] = '\0';
if ( (ptr = strchr(buff, ' ')) == NULL) {
err_sys("bogus request: %s", buff);
continue;
}
// ptr增1后即指向后跟的路径名的首字符
*ptr++ = 0;
pid = atol(buff);
snprintf(fifoname, sizeof(fifoname), "/tmp/fifo.%ld", (long)pid);
// 尝试打开客户请求的文件
if ( (writefifo = open(fifoname, O_WRONLY, 0)) < 0) {
err_msg("cannot open: %s", fifoname);
continue;
}
if ( (fd = open(ptr, O_RDONLY)) < 0) {
snprintf(buff + n, sizeof(buff)-n, ":can't open, %s\n",
strerror(errno));
n = strlen(ptr);
Write(writefifo, ptr, n);
Close(writefifo);
} else {
while ( (n = Read(fd, buff, MAXLINE)) > 0)
Write(writefifo, buff, n);
Close(fd);
Close(writefifo);
}
}
exit(0);
}
8.2 客户端程序
#include "fifo.h"
int main(int argc, char const *argv[])
{
int readfifo, writefifo;
ssize_t n;
size_t len;
char *ptr, buff[MAXLINE], fifoname[MAXLINE];
pid_t pid;
pid = getpid();
snprintf(fifoname, sizeof(fifoname), "/tmp/fifo.%ld", (long)pid);
if ( (mkfifo(fifoname, FILE_MODE) < 0) && (errno != EEXIST))
err_sys("can't create %s", fifoname);
// start buffer with pid and a blank
snprintf(buff, sizeof(buff), "%ld", (long)pid);
len = strlen(buff);
ptr = buff + len;
// read pathname
Fgets(ptr, MAXLINE-len, stdin);
len = strlen(buff);
// open FIFO to server and write PID and pathname to FIFO
writefifo = Open(SERV_FIFO, O_WRONLY, 0);
Write(writefifo, buff, len);
// now open our FIFO; blocks until server opens for writing
readfifo = Open(fifoname, O_RDONLY, 0);
// read from IPC, write to standard output
while ( (n = Read(readfifo, buff, MAXLINE)) > 0)
Write(STDOUT_FILENO, buff, n);
Close(readfifo);
Unlink(fifoname);
exit(0);
}
9 字节流与信息
- 字节流I/O模型对读写操作不进行数据检查,例如,从某个FIFO中读出100字节的进程无法判断次100字节是执行了单个100字节的写操作,还是5个20字节的写操作,或者其他。
- 有时候应用希望对所传送的数据加上某种结构,以知道获得信息的边界。下述3种技巧可用于这种目的:
- 带内特殊终止序列 :写进程给每个信息添加一个换行符,读进程则每次读出一行。
- 显式长度;
- mymesg结构
#include "unpipc.h"
// want sizeof(struct mymesg) <= PIPE_BUF
#define MAXMESGDATA (PIPE_BUF - 2*sizeof(long))
#define MESGHDRSIZE (sizeof(struct mymesg) - MAXMESGDATA)
struct mymesg {
long mesg_len; // can be 0
long mesg_type; // must be > 0
char mesg_data[MAXMESGDATA];
};
ssize_t mesg_send(int, struct mymesg *);
void Mesg_send(int, struct mymesg *);
ssize_t mesg_recv(int, struct mymesg *);
ssize_t Mesg_recv(int, struct mymesg *);
ssize_t mesg_send(int fd, struct mymesg *mptr)
{
return (write(fd, mptr, MESGHDRSIZE + mptr -> mesg_len));
}
ssize_t mesg_recv(int fd, struct mymesg *mptr)
{
size_t len;
ssize_t n;
// read message header first, to get len of data that follows
if ( (n = Read(fd, mptr, MESGHDRSIZE)) == 0)
// Note:return(0)是返回值为0, return 0 是终止程序
return(0);
else if (n != MESGHDRSIZE)
err_quit("message header: expected %d, got%d", MESGHDRSIZE, n);
if ( (len = mptr->mesg_len) > 0)
if ( (n = Read(fd, mptr->mesg_data, len)) != len)
err_quit("message data: expected %d, got %d", len, n);
return(len);
}
10 习题
-
4.2 我们一般是先调用mkfifo,检查是否返回EEXIST错误,若是则调用open。若先调用open,再调用mkfifo,情况又如何?
如果调用关系反转了,另外某个进程就有可能在本进程的open和mkfifo两个调用之间创建本进程想要创建的FIFO,结果导致本进程的mkfifo调用失败。
-
4.5 当服务器启动后,它阻塞在自己的第一个open调用中,直到客户的第一个open打开同一个FIFO用于写为止。我们怎样才能绕过这样的阻塞,使得两个open都立刻返回,转而阻塞正在首次调用readline上?
把第一个open调用改为指定非阻塞标志 :
readfifo = Open(SERV_FIFO, O_RDONLY | O_NONBLOCK, 0); 该调用将立即返回,接下去的open调用(用于只写)也立即返回,因为它要打开的FIFO已经由第一个open调用打开用于读。但是为了避免从readline返回错误,描述符readfifo的O_NONBLOCK标志必须在调用readline之前关掉。
-
4.7 为什么在读进程关闭管道或FIFO之后给写进程产生一个信号,而不会在写进程关闭管道或FIFO之后给读进程产生一个信号?
写进程关闭管道或FIFO的信息通过文件结束符传递给读进程。