UNIX IPC有多种形式,最初使用的便是管道pipe,管道没有名字,又称匿名管道,一般用于有亲缘关系的进程间通信,后来出现了fifo这种管道,它是有名字的,又叫做有名管道,可用于无亲缘关系的进程间通信,这两种管道的数据传输都可以使用我们最熟悉的write、read函数来完成。
创建管道使用如下pipe函数:
#include <unistd.h>
int pipe(int pipefd[2]);
pipe函数打开两个文件描述符,pipefd[0]用于读,pipefd[1]用于写,管道创建成功时返回0,否则返回-1,并设置errno。管道提供了一个单向数据流,一般用于父子进程间,两个进程分别关掉它们的读、写文件描述符,如果需要一个双向数据流,则需要创建两个管道,下面举例说明。
// mainpipe.c
/******************************
** A Client-Server Program
** Using fork and two pipes
** Client: father process, input a pathname
** Server: child process, open the pathname and write back to client
******************************/
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/wait.h>
#include <unistd.h>
#define MAXLINE (100) // buffer size
// client/server, readfd for read, writefd for write
void client(int readfd, int writefd);
void server(int readfd, int writefd);
int main(int argc, char **argv)
{
int pipe1[2];
int pipe2[2];
pid_t childfd;
if (-1 == pipe(pipe1) || -1 == pipe(pipe2)) { // create two pipes
printf("create pipe error: %s", strerror(errno));
exit(EXIT_FAILURE);
}
if (0 == (childfd = fork())) { // child
// using pipe1[0] for read and pipe2[1] for write
// so close unused pipe1[1] and pipe2[0]
if (-1 == close(pipe1[1]) || -1 == close(pipe2[0])) {
printf("close pipe error: %s", strerror(errno));
exit(EXIT_FAILURE);
}
server(pipe1[0], pipe2[1]); // child is a server
exit(EXIT_SUCCESS);
}
// using pipe2[0] for read and pipe1[1] for write
// so close unused pipe1[0] and pipe2[1]
if (-1 == close(pipe1[0]) || -1 == close(pipe2[1])) {
printf("close pipe error: %s", strerror(errno));
exit(EXIT_FAILURE);
}
client(pipe2[0], pipe1[1]); // father is a client
if (-1 == waitpid(childfd, NULL, 0)) { // wait for child to terminate
perror("waitpid error");
exit(EXIT_FAILURE);
}
exit(EXIT_SUCCESS);
}
void client(int readfd, int writefd)
{
size_t len;
ssize_t n;
char buff[MAXLINE] = { 0 };
fgets(buff, MAXLINE, stdin); // using fgets to input from stdin
len = strlen(buff);
if ('\n' == buff[len - 1]) {
--len; // delete newline from fgets
}
if (-1 == write(writefd, buff, len)) { // write pathname to pipe
printf("write error: %s", strerror(errno));
}
while (0 < (n = read(readfd, buff, MAXLINE))) { // read data from pipe
if (-1 == write(STDOUT_FILENO, buff, n)) {
printf("write error: %s", strerror(errno)); // write data to stdout
}
}
}
void server(int readfd, int writefd)
{
int fd;
ssize_t n;
char buff[MAXLINE + 1] = { 0 };
if (-1 == (n = read(readfd, buff, MAXLINE))) { // read pathname from pipe
printf("read error: %s", strerror(errno));
}
else if (0 == n) {
perror("end-of-file while reading pathname");
}
buff[n] = '\0'; // null terminate
if (-1 == (fd = open(buff, O_RDONLY))) {
perror("open error");
snprintf(buff + n, sizeof(buff) - n, ": can't oepn, %s\n", strerror(errno));
n = strlen(buff);
write(writefd, buff, n); // open failed and tell client
}
else {
while (0 < (n = read(fd, buff, MAXLINE))) { // open succeeded and copy file to pipe
if (-1 == write(writefd, buff, n)) {
printf("write error: %s", strerror(errno));
}
}
if (-1 == close(fd)) {
printf("close fd error: %s", strerror(errno));
}
}
}
mainpipe.c是一个简单的Client-Server程序,Client通过Server获取文件信息。为了实现双向数据流,即Client输入文件名给Server,而Server又打开这个文件并传输文件内容给Client,这里创建了两个管道。fork之后,子进程为Client,父进程为Server,父子进程分别关闭它们不需要的文件描述符,然后开始通信。
上面提到的管道是一个半双工管道,也就是说一个文件描述符只能用于读,一个文件描述符只能用于写,如果拿某个文件描述符同时进行读写,把它当作全双工管道来用,就会出错,strerror(errno)的输出为“Bad file descriptor”,不过有的系统支持全双工管道的pipe,如SVR4,而socketpair则是具有全双工管道功能的另一个函数。
如果我们在shell终端输入一些支持管道的命令,比如说“ls | sort”,这也是一种单向数据流,半双工管道,把ls的标准输出(一个进程)复制到了sort的标准输入(另一个进程)。
stdio.h中还提供了另一个使用管道的函数popen,它创建一个管道并启动另外一个进程,就好像函数内部封装了pipe和fork一样,该管道也是单双工的,或者读,或者写。
#include <stdio.h>
FILE *popen(const char *command, const char *type);
int pclose(FILE *stream);
popen的command参数是一个shell命令,type可以是r或者w,表示读或者写,pclose用于关闭打开的文件流。下面以一个例子说明popen的用法,实现shell的cat命令,所以type参数应该为r。
// mainpopen.c
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#define MAXLINE (100) // buffer size
int main(int argc, char **argv)
{
size_t n;
char buff[MAXLINE];
char command[MAXLINE];
FILE *fp;
fgets(buff, MAXLINE, stdin); // get file name
n = strlen(buff);
if ('\n' == buff[n - 1]) {
--n; // delete newline from fgets
}
snprintf(command, sizeof(command), "cat %s", buff); // shell command
if (NULL == (fp = popen(command, "r"))) { // create pipe for read
perror("popen error");
exit(EXIT_FAILURE);
}
while(NULL != fgets(buff, MAXLINE, fp)) {
fputs(buff, stdout);
}
pclose(fp); // close from popen
exit(EXIT_SUCCESS);
}
fifo即first in first out,类似于pipe,也是半双工的单向数据流,与pipe不同的是它关联一个路径名,因此也叫做有名管道,可以用于没有亲缘关系的进程间通信。下面是与fifo相关的几个函数。
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int mkfifo(const char *pathname, mode_t mode);
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
int unlink(const char *pathname);
创建fifo使用mkfifo函数(在shell终端可使用mkfifo命令创建fifo),pathname是这个fifo的路径名,mode为文件权限模式,类似于open函数的第三个参数,受当前进程掩码umask的影响,其真正的权限为(mode & ~umask),创建成功时返回0,失败时返回-1并设置相应的errno。创建失败时,有可能是pathname已经存在了,这时会产生一个EEXIST错误,等同于open函数的flags参数设置了(O_CREAT | O_EXCL)一样,要么创建,要么返回文件已经存在的错误。如果pathname已经存在,可使用open函数来打开并进行IPC通信,open打开时只能是读或者写,即O_RDONLY或者O_WRONLY,而不能是O_RDWR。通信完毕后,从文件系统中删除pathname,使用unlink函数。
下面使用fifo来代替上面Client-Server程序中的pipe,client和server函数不变,只需修改main函数即可。
// mainfifo.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#define MAXLINE (100) // buffer size
#define FIFO1 "/tmp/fifo.1"
#define FIFO2 "/tmp/fifo.2"
#define FILE_MODE (S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)
// client/server, readfd for read, writefd for write
void client(int readfd, int writefd);
void server(int readfd, int writefd);
int main(int argc, char **argv)
{
int readfd;
int writefd;
pid_t childpid;
if ((0 > mkfifo(FIFO1, FILE_MODE)) && (EEXIST != errno)) { // make fifo, errno can be EEXIST
printf("mkfifo %s error: %s", FIFO1, strerror(errno));
exit(EXIT_FAILURE);
}
if ((0 > mkfifo(FIFO2, FILE_MODE)) && (EEXIST != errno)) { // make fifo, errno can be EEXIST
printf("mkfifo %s error: %s", FIFO2, strerror(errno));
unlink(FIFO1); // remove unuesd fifo
exit(EXIT_FAILURE);
}
if (0 == (childpid = fork())) { // child
if (0 > (readfd = open(FIFO1, O_RDONLY, 0))) {
printf("open %s error: %s", FIFO1, strerror(errno));
}
if (0 > (writefd = open(FIFO2, O_WRONLY, 0))) {
printf("open %s error: %s", FIFO2, strerror(errno));
}
server(readfd, writefd); // child is a server
exit(EXIT_SUCCESS);
}
if (0 > (writefd = open(FIFO1, O_WRONLY, 0))) {
printf("open %s error: %s", FIFO1, strerror(errno));
}
if (0 > (readfd = open(FIFO2, O_RDONLY, 0))) {
printf("open %s error: %s", FIFO2, strerror(errno));
}
client(readfd, writefd); // father is a client
if (-1 == waitpid(childpid, NULL, 0)) { // wait for child to terminate
perror("waitpid error");
}
close(readfd);
close(writefd);
unlink(FIFO1);
unlink(FIFO2);
exit(EXIT_SUCCESS);
}
void client(int readfd, int writefd)
{
size_t len;
ssize_t n;
char buff[MAXLINE] = { 0 };
fgets(buff, MAXLINE, stdin); // using fgets to input from stdin
len = strlen(buff);
if ('\n' == buff[len - 1]) {
--len; // delete newline from fgets
}
if (-1 == write(writefd, buff, len)) { // write pathname to pipe
printf("write error: %s", strerror(errno));
}
while (0 < (n = read(readfd, buff, MAXLINE))) { // read data from pipe
if (-1 == write(STDOUT_FILENO, buff, n)) {
printf("write error: %s", strerror(errno)); // write data to stdout
}
}
}
void server(int readfd, int writefd)
{
int fd;
ssize_t n;
char buff[MAXLINE + 1] = { 0 };
if (-1 == (n = read(readfd, buff, MAXLINE))) { // read pathname from pipe
printf("read error: %s", strerror(errno));
}
else if (0 == n) {
perror("end-of-file while reading pathname");
}
buff[n] = '\0'; // null terminate
if (-1 == (fd = open(buff, O_RDONLY))) {
perror("open error");
snprintf(buff + n, sizeof(buff) - n, ": can't oepn, %s\n", strerror(errno));
n = strlen(buff);
write(writefd, buff, n); // open failed and tell client
}
else {
while (0 < (n = read(fd, buff, MAXLINE))) { // open succeeded and copy file to pipe
if (-1 == write(writefd, buff, n)) {
printf("write error: %s", strerror(errno));
}
}
if (-1 == close(fd)) {
printf("close fd error: %s", strerror(errno));
}
}
}
上面例子中的fifo应用是在父子进程间进行的,下面再对其进行修改,在两个无亲缘关系的进程间完成。
// server_main.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#define MAXLINE (100) // buffer size
#define FIFO1 "/tmp/fifo.1"
#define FIFO2 "/tmp/fifo.2"
#define FILE_MODE (S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)
// server, readfd for read, writefd for write
void server(int readfd, int writefd);
int main(int argc, char **argv)
{
int readfd;
int writefd;
if ((0 > mkfifo(FIFO1, FILE_MODE)) && (EEXIST != errno)) { // make fifo, errno can be EEXIST
printf("mkfifo %s error: %s", FIFO1, strerror(errno));
exit(EXIT_FAILURE);
}
if ((0 > mkfifo(FIFO2, FILE_MODE)) && (EEXIST != errno)) { // make fifo, errno can be EEXIST
printf("mkfifo %s error: %s", FIFO2, strerror(errno));
unlink(FIFO1); // remove unuesd fifo
exit(EXIT_FAILURE);
}
if (0 > (readfd = open(FIFO1, O_RDONLY, 0))) {
printf("open %s error: %s", FIFO1, strerror(errno));
}
if (0 > (writefd = open(FIFO2, O_WRONLY, 0))) {
printf("open %s error: %s", FIFO2, strerror(errno));
}
server(readfd, writefd); // child is a server
exit(EXIT_SUCCESS);
}
void server(int readfd, int writefd)
{
int fd;
ssize_t n;
char buff[MAXLINE + 1] = { 0 };
if (-1 == (n = read(readfd, buff, MAXLINE))) { // read pathname from pipe
printf("read error: %s", strerror(errno));
}
else if (0 == n) {
perror("end-of-file while reading pathname");
}
buff[n] = '\0'; // null terminate
if (-1 == (fd = open(buff, O_RDONLY))) {
perror("open error");
snprintf(buff + n, sizeof(buff) - n, ": can't oepn, %s\n", strerror(errno));
n = strlen(buff);
write(writefd, buff, n); // open failed and tell client
}
else {
while (0 < (n = read(fd, buff, MAXLINE))) { // open succeeded and copy file to pipe
if (-1 == write(writefd, buff, n)) {
printf("write error: %s", strerror(errno));
}
}
if (-1 == close(fd)) {
printf("close fd error: %s", strerror(errno));
}
}
}
// client_main.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#define MAXLINE (100) // buffer size
#define FIFO1 "/tmp/fifo.1"
#define FIFO2 "/tmp/fifo.2"
#define FILE_MODE (S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)
// client readfd for read, writefd for write
void client(int readfd, int writefd);
int main(int argc, char **argv)
{
int readfd;
int writefd;
if (0 > (writefd = open(FIFO1, O_WRONLY, 0))) {
printf("open %s error: %s", FIFO1, strerror(errno));
}
if (0 > (readfd = open(FIFO2, O_RDONLY, 0))) {
printf("open %s error: %s", FIFO2, strerror(errno));
}
client(readfd, writefd); // father is a client
close(readfd);
close(writefd);
unlink(FIFO1);
unlink(FIFO2);
exit(EXIT_SUCCESS);
}
void client(int readfd, int writefd)
{
size_t len;
ssize_t n;
char buff[MAXLINE] = { 0 };
fgets(buff, MAXLINE, stdin); // using fgets to input from stdin
len = strlen(buff);
if ('\n' == buff[len - 1]) {
--len; // delete newline from fgets
}
if (-1 == write(writefd, buff, len)) { // write pathname to pipe
printf("write error: %s", strerror(errno));
}
while (0 < (n = read(readfd, buff, MAXLINE))) { // read data from pipe
if (-1 == write(STDOUT_FILENO, buff, n)) {
printf("write error: %s", strerror(errno)); // write data to stdout
}
}
}
然后就可以在shell终端测试了:
$ gcc -o server server_main.c
$ gcc -o client client_main.c
$ ./sever &
$ ./client
上面的例子只是简单的介绍了pipe和fifo的用法,其实在使用过程中,还有一些东西是需要注意的。
阻塞与否——
设置非阻塞时,可以通过open函数的O_NONBLOCK标志实现,也可以通过fcntl函数完成,使用fcntl时一般先通过F_GETFL命令获取文件当前状态flags,然后再将(flags |= O_NONBLOCK)通过G_SETFL命令设置非阻塞,否则可能会出问题。
OPEN_MAX——
一个进程在任意时刻打开文件描述符的最大个数,其值可通过sysconf(_SC_OPEN_MAX)函数查询,使用setrlimit函数来修改,在shell终端也可以使用”getconf OPEN_MAX”命令来查询,通过ulimit命令修改。
PIPE_BUF——
可原子地写往pipe或fifo的最大数据量,其值可使用pathconf(path, _PC_PIPE_BUF)或fpathconf函数获得,在shell终端使用”getconf PIPE_BUF ”命令也可以查询某个path的PIPE_BUF。