Linux 网络编程-进程管道

目录

4.1 管道

4.3.1 用 C 来建立、使用管道

4.3.2 需要注意的问题

4.2 有名管道

4.4.1 有名管道的创建

4.4.2 有名管道的 I/O 使用

4.4.3 未提到的关于有名管道的一些注意



4.1 管道


简单的说,管道就是将一个程序的输出和另外一个程序的输入连接起来的单向通道。它是 UNIX/Linux 系统的各种进程通信方法中,最古老而应用最为广泛的一种(特别是在 shell 中)。
#ls -l|more
在上面的例子中我们建立了这样的一个管道:获取 ls -l 的输出,再将其作为 more 命令的输入。形象的说,就是数据沿着管道从管道的左边流到了管道的右边。
这个例子并不复杂,只要是对 Linux/UNIX 比较熟悉的人都曾经使用过类似的命令。但是,在简单的命令底下,Linux/UNIX 内核究竟都做了些什么呢?
当进程创建一个管道的时候,系统内核同时为该进程设立了一对文件句柄(一个流), 一个用来从该管道获取数据(read),另一个则用来做向管道的输出(write)。
图 4-1 显示了进程和管道间的相互作用。
从图 4-1 中可以清楚的看出进程和管道是如何通过句柄进行数据交换的。进程通过句柄 fd[0]向管道写入(write)数据,同时通过 fd[1]从管道读出(read)数据。到这里有人也许会想起 UNIX 的文件处理。事实上,在 Linux 系统内核里,每个管道都是用一个 inode 节点来表示的。(当然,你是不会看到这个节点的,它只存在于系统的内核中。)理解了这一点,我们就可以容易的掌握接下来要讲的管道的 I/O 处理了。
不过,到目前为止,我们所建立的管道模型还没有任何的实际意义。因为这个管道只被用来同单个进程通信。建立一个同自己通信的管道有什么用处呢?为了解决这个问题,我们在主进程中利用 fork()函数创建一个自身的自进程。大家也许还记得,fork()的子进程自动继承了父进程打开的文件句柄。利用继承的句柄,就可以实现父/子间的通信了。这个关系可以用图 4-2 来表示:
 



图 4-1 进程和管道间的相互作用

 




图 4-2 fork()函数调用后的管道


现在,父子两个进程同时拥有对同一个管道的读写句柄。因为管道必须是单向的(因为它没有提供锁定的保护机制),所以我们必须决定数据的流动方向(从父到子,还是从子到父?),然后在每个进程中关闭不需要的句柄。假设我们需要管道从子进程向父进程传送数据,关闭了相应句柄后的管道可以用图 4-3 来表示。
 



图 4-3 关闭了相应句柄后的管道

这样,一个完整的管道就被建立了。下面我们就可以使用 read()和 write()函数来对它进行读写操作了。关于这两个函数的具体使用在 UNIX 的文件函数中有介绍,读者可以自行参阅相应的资料。




4.3.1  C 来建立、使用管道



以上我们介绍了管道的概念和它在 Linux 系统中的模型。下面,我们就将开始用 C 来建立自己的管道。
1. Pipe()函数
在 C 程序中,我们使用系统函数 pipe()来建立管道。它只有一个参数:一个有两个成员的整型数组,用于存放 pipe()函数新建立的管道句柄。其函数原型如下:
系统调用: pipe();
函数声明: int pipe( int fd[2] );
返回值: 0 on success
-1 on error: errno = EMFILE (no free descriptors)
EMFILE (system file table is full) EFAULT (fd array is not valid)

注意: fd[0] 用来从管道中读, fd[1] 用来向管道中写数组中的第一个元素(fd[0])是从管道中读出数据的句柄,第二个元素(fd[1])是向管道写入数据的句柄。也即是说,fd[1]的写入由fd[0]读出。
在建立了管道之后,我们使用 fork()函数建立一个子线程:

#include <stdio.h> #include <unistd.h> #include <sys/types.h>
 
 
main()
{
int fd[2];
pid_t childpid; pipe(fd);
if((childpid = fork()) == -1)
{
perror("fork"); exit(1);
}
 
 
 
 
}



接下来,我们假设需要管道中数据的流动是从子进程到父进程。这样父进程就需要关闭(close())写管道的句柄(fd[1]),而子进程需要关闭读管道的进程(fd[0])。
注意:因为父子进程同时拥有读写句柄,为了避免不必要的麻烦,我们在程序中务必要记住关闭不需要的句柄!

#include <stdio.h> #include <unistd.h> #include <sys/types.h>
 
main()
{
int fd[2];
pid_t childpid; pipe(fd);
if((childpid = fork()) == -1)
{
perror(“fork”); exit(1);

 
}
 
if(childpid == 0)
{

 
 
 
}
else
{
/* 子进程关闭管道的读句柄 */ close(fd[0]);
 
 
 
/* 父进程关闭管道的写句柄 */ close(fd[1]);

} .
 
 
 
 
 
}


管道建立之后,我们就可以像操作普通文件一样对其进行操作:

#include <stdio.h> #include <unistd.h> #include <sys/types.h>
 
int main(void)
{
int fd[2], nbytes;
pid_t childpid;
char string[] = “Hello, world!\n”; char  readbuffer[80];
 
pipe(fd);
if((childpid = fork()) == -1)
{
perror(“fork”); exit(1);
}
 
if(childpid == 0)
{
/* 子进程关闭管道的读句柄 */ close(fd[0]);
/* 通过写句柄向管道写入信息 */ write(fd[1], string, strlen(string));
_exit(0);
}

 
else
{
/* 父进程关闭管道的写句柄 */ close(fd[1]);
/* 通过读句柄从管道读出信息 */
nbytes = read(fd[0], readbuffer, sizeof(readbuffer)); printf(“Received string: %s”, readbuffer);
}
 
return(0);
 
}


2. 使用 dup()函数
有时候我们需要将子进程当中的管道的句柄定向到标准 I/O(stdin/stdout)上去。这样, 在子进程中使用 exec()函数调用外部程序时,这个外部程序就会将管道作为它的输入/输出。这个过程可以用系统函数 dup()来实现。其函数原型如下:
系统调用: dup();
函数声明: int dup( int oldfd );
返回值: new descriptor on success
-1 on error: errno = EBADF (oldfd is not a valid descriptor)
EBADF (newfd is out of range)
EMFILE (too many descriptors for the process)
注意: 旧句柄没有被关闭,新旧两个句柄可以互换使用
虽然原句柄和新句柄是可以互换使用的,但为了避免混淆,我们通常会将原句柄关闭
(close)。同时要注意,在 dup()函数中我们无法指定重定向的新句柄,系统将自动使用未被使用的最小的文件句柄(记住,句柄是一个整型量)作为重定向的新句柄。请看下面的例子:

pipe(fd);
 
childpid = fork(); if(childpid == 0)
{
 
/* 关闭子进程的文件句柄 0(stdin) */ close(0);
 
/* 将管道的读句柄定义到 stdin */ dup(fd[0]);
execlp(“sort”, “sort”, NULL);
 
}


在上例中巧妙的利用了 dup()函数的特性。因为文件句柄 0(stdin)被关闭了,对 dup 函数的调用就将管道读句柄 fd[0]定向到了 stdin(因为句柄 0 是最小的未用句柄)。然后我们调用 execlp 函数,用外部过程 sort 覆盖了子进程的代码。因为它继承了子进程的基本输入/输出,所以它就将管道作为了它的输入。现在,我们在父进程里向管道写入的任何数据都将自动被 sort 接受进行排序


3. 使用 dup2()函数
在 Linux 系统中还有一个系统函数 dup2()。单从函数名上我们也可以判断出它和 dup()
函数的渊源。下面是它的原型:
系统调用: dup2();
函数声明: int dup2( int oldfd, int newfd );
返回值: new descriptor on success
-1 on error: errno = EBADF (oldfd is not a valid descriptor)
EBADF (newfd is out of range)
EMFILE(too many descriptors for the process)
注意: 旧句柄将被 dup2()自动关闭
显然,原来的 close 以及 dup 这一套调用现在全部由 dup2()来完成。这样不仅简便了程序,更重要的是,它保证了操作的独立性和完整性,不会被外来的信号所中断。在原来的dup()调用中,我们必须先调用close()函数。假设此时恰好一个信号使接下来的 dup()调用不能立即执行,这就会引发错误(进程没有了 stdin )。使用 dup2()就不会有这样的危险。下面的例子演示了 dup2()函数的使用:

 
pipe(fd);
. childpid = fork(); if(childpid == 0)
{
/* 将管道的读入端定向到 stdin */ dup2(0, fd[0]);
execlp("sort", "sort", NULL);
 
}


4. 使用 popen()/pclose()函数
看了 dup2()函数,一定有人会想,既然能把 close 和 dup 合成一个函数,那么有没有把
fork、exec 和 dup()结合的函数呢?答案是肯定的。它就是 linux 的系统函数 popen(): 库函数: popen();
函数声明: FILE *popen ( char *command, char *type);
返回值: new file stream on success
NULL on unsuccessful fork() or pipe() call
NOTES: creates a pipe, and performs fork/exec operations using "command" popen()函数首先调用 pipe()函数建立一个管道,然后它用 fork()函数建立一个子进程,
运行一个 shell 环境,然后在这个 shell 环境中运行"command"参数指定的程序。数据在管道中流向由"type"参数控制。这个参数可以是"r"或者"w",分别代表读和写。需要注意的是, "r"和"w"两个参数不能同时使用!在 Linux 系统中,popen 函数将只使用"type"参数中第一个字符,也就是说,使用"rw"和"r"作为"type"参数的效果是一样的,管道将只打开成读状态。
使用 popen 打开的管道必须用 pclose()函数来关闭。还记得 fopen 和 fclose 的配对使用
吗?这里再次显示了管道和文件的相似性。

库函数: pclose();
函数声明: int pclose( FILE *stream );
返回值: exit status of wait4() call
-1 if "stream" is not valid, or if wait4() fails
NOTES: waits on the pipe process to terminate, then closes the stream.
下面是一个使用 popen/pclose 的例子:
#include <stdio.h> #define MAXSTRS 5
int main(void)
{
int cntr;
FILE *pipe_fp;
char *strings[MAXSTRS] = { "roy", "zixia", "gouki","supper", "mmwan"};
 
/* 用 popen 建立管道 */
if (( pipe_fp = popen("sort", "w")) == NULL)
{
perror("popen"); exit(1);
}
 
/* Processing loop */ for(cntr=0; cntr<MAXSTRS; cntr++)
{
fputs(strings[cntr], pipe_fp); fputc('\n', pipe_fp);
}
 
/* 关闭管道 */ pclose(pipe_fp); return(0);
}


使用 popen()函数除了节省源代码之外,它还有一个优点:你可以在"command"中使用任意合法的 shell 指令,包括重定向和管道!下面的几个例子都是合法的 popen 调用:
popen("ls ~roy", "r"); popen("sort > /tmp/zixia", "w"); popen("sort | uniq | more", "w");
下面是一个稍微复杂一点的例子,在里面建立了两个管道:

#include <stdio.h>
 
int main(void)
{
FILE *pipein_fp, *pipeout_fp; char readbuf[80];

 
/* 用 popen 建立一个通向"ls:的读管道 */ if (( pipein_fp = popen("ls", "r")) == NULL)
{
perror("popen"); exit(1);
}
 
/* 用 popen 建立一个通向"sort"的写管道 */ if (( pipeout_fp = popen("sort", "w")) == NULL)
{
perror("popen"); exit(1);
}
 
/* 进 程 循 环 */ while(fgets(readbuf, 80, pipein_fp))
fputs(readbuf, pipeout_fp);
 
/* 关闭打开的管道 */ pclose(pipein_fp); pclose(pipeout_fp);
 
return(0);
}


最后,为了更好的理解管道,我们给出一个 popen()和 fopen()混合使用的例子,请读者与上例对照,自行分析管道与文件处理的异同:

#include <stdio.h>
 
int main(int argc, char *argv[])
{
FILE *pipe_fp, *infile; char readbuf[80];
 
if( argc != 3) {
fprintf(stderr, "USAGE: popen3 [command] [filename]\n"); exit(1);
}
 
/* 打开输入文件 */
if (( infile = fopen(argv[2], "rt")) == NULL)
{
perror("fopen"); exit(1);
}

 
 
/* 建立写管道 */
if (( pipe_fp = popen(argv[1], "w")) == NULL)
{
perror("popen"); exit(1);
}
 
/* Processing loop */ do
fgets(readbuf, 80, infile); if(feof(infile)) break;
 
fputs(readbuf, pipe_fp);
} while(!feof(infile));
 
fclose(infile); pclose(pipe_fp);
 
return(0);
}



4.3.2 需要注意的问题



以下是一些在管道的使用中需要注意的问题: 1.pipe()的调用必须在 fork()之前; 2.及时关闭不需要的管道句柄;
1. 使用 dup()之前确定定向的目标是最小的文件句柄;
2. 管道只能实现父子进程间的通信,如果两个进程之间没有 fork()关系,就必须考虑其他的进程通信方法。




4.2 有名管道



为了解决管道不能提供非父/子关系进程间通信的缺陷,在管道的基础上发展了有名管道(FIFOs)的概念。我们知道,尽管管道在 Linux 系统内部是以文件节点(inode)的形式存在的,但是由于其对外的不可见性(“无名”性),我们无法创建新的句柄对其进行访问。而有名管道在 Linux 系统中以一种特殊的设备文件的形式存在于文件系统中。这样它不仅具有了管道的通信功能,也具有了普通文件的优点(可以同时被多个进程共享,可以长期存在等等),有效的解决了管道通信的缺点。




4.4.1 有名管道的创建



因为有名管道是存在于文件系统中的文件节点,所以我们可以用建立文件节点的方式来建立有名管道。在 shell 中我们可以用下面的命令:
#mknod sampleFIFO p #mkfifo – m 0666 sampleFIFO
以上的两个命令是等价的,它们都会在当前的文件系统中建立一个名字为 samlpeFIFO 的有名管道。不过,在细节上他们还是有差别的。mkfifo 命令可以用“-m”选项指定所建立的有名管道的存取权限,而 mknod 则需要之后使用 chmod 来改变有名管道的存取权限。
通过文件列表信息中的 p 指示符我们可以迅速的辨认出有名管道。例如:
#ls -l
prw-r--r-- 1 root root 0 May 14 16:25 sampleFIFO|
在 C 中我们通过系统函数 mknod 来建立有名管道: 库函数: mknod();
函数声明: int mknod( char *pathname, mode_t mode, dev_t dev);
返回值: 0 on success,
-1 on error: errno = EFAULT (pathname invalid)
EACCES (permission denied) ENAMETOOLONG (pathname too long) ENOENT (invalid pathname)
ENOTDIR (invalid pathname)
(see man page for mknod for others) NOTES: Creates a filesystem node (file, device file, or FIFO)
下面是个简单的例子:
mknod(“/tmp/sampleFIFO”,s_IFIFO|0666,0)
这条语句在文件系统中建立了一个名为”/tmp/sampleFIFO”的有名管道,其读写权限是0666(当然,最终的权限还和你的 umask 值有关)。mknod 的第三个参数在创建有名管道时被忽略,一般都填零。




4.4.2 有名管道的 I/O 使用



有名管道和管道的操作是相同的,只是要注意,在引用已经存在的有名管道时,首先要用系统中的文件函数来打开它,才能接下来进行其他的操作。例如,我们可以用操作文件流的 fopen()和fclose()来打开一个有名管道。下面是一个 server 方的例子:

/* fifoserver.c */ #include <stdio.h> #include <stdlib.h> #include <sys/stat.h> #include <unistd.h> #include <linux/stat.h>
#define FIFO_FILE "sampleFIFO" int main(void)
{
FILE *fp;
char readbuf[80];
 
/* Create the FIFO if it does not exist */

 
umask(0);
 
/*在文件系统中创建有名管道*/ mknod(FIFO_FILE, S_IFIFO|0666, 0);
 
while(1)
{
 
/*打开有名管道*/
fp = fopen(FIFO_FILE, "r");
 
/*从有名管道中读取数据*/ fgets(readbuf, 80, fp);
printf("Received string: %s\n", readbuf);
 

 
 
 
}
return(0);
}
/*关闭有名管道*/ fclose(fp);

因为有名管道自动支持进程阻塞,所以我们可以让这个 server 在后台运行:
#fifoserver &
然后运行下面的 client 程序:

#include <stdio.h> #include <stdlib.h>
#define FIFO_FILE "sampleFIFO" int main(int argc, char *argv[])
{
FILE *fp;
 
if ( argc != 2 ) {
printf("USAGE: fifoclient [string]\n"); exit(1);
}
/*打开有名管道*/
if((fp = fopen(FIFO_FILE, "w")) == NULL) { perror("fopen");
exit(1);
}
/*向有名管道中写入数据*/
fputs(argv[1], fp);
/*关闭有名管道*/

 
fclose(fp); return(0);
}

由于有名管道的自动阻塞特性,当上面的 server 打开一个有名管道准备读入时,server

进程就会被阻塞以等待其他进程(在这里是我们的 client 进程)在有名管道中写入数据。反之亦然。不过,如果需要,我们也可以在打开一个有名管道时使用 O_NONBLOCK 标志来关闭它的自动阻塞特性。


4.4.3 未提到的关于有名管道的一些注意

首先,有名管道必须同时有读/写两个进程端。如果一个进程试图向一个没有读入端进程的有名管道写入数据,一个 SIGPIPE 信号就会产生。这在涉及多个进程的有名管道通信中是很有用的。

其次,关于管道操作的独立性。一个“独立”的操作意味着,这个操作不会因为任何

原因而被中断。比如,在 POSIX 标准中,头文件/usr/include/posix1_lim.h 中定义了在一次独立的管道读/写操作中最大传输的数据量(buffer size):

#define _POSIX_PIPE_BUF 512

也即是说,在一次独立的管道读/写操作中最多只能传送 512 个字节的数据,当数据量超过这个上限时操作就只能被分成多次独立的读/写操作。在 Linux 系统中,头文件“linux/limits.h” 中定义了类似的限制:

#define PIPE_BUF 4096

可以看出,和 POSIX 标准比,上限被大大增加了。这在涉及多进程的有名管道操作中是非常重要的。如果在某个进程的一次写操作中传输的数据量超过了独立读/写操作的数据量上限,这个操作就有可能被别的进程的写操作打断。也就是说,别的进程把数据插入了该进程写入管道的数据序列中从而造成混乱。这是在有名管道应用中需要特别注意的。


篇幅有限,关于进程间管道先讲到这,后续教学会在最近的后续篇章中更新,请大家关注。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值