文件描述符管理、进程间通信(管道)
首先介绍下文件描述符: 在linux下用文件描述符来表示设备文件和普通文件。文件描述符是一个整形的数据,所有对文件的操作都是通过文件描述符实现。文件描述符是文件系统中链接用户空间和内核空间的枢纽。当打开一个或创建一个文件时,内核创建相应的结构,并生成一个整形的变量传送给用户空间的对应进程。进程用这个文件描述符来对文件进行操作。用户空间的文件操作,例如读、写一个文件时,将文件描述符传送
首先介绍下文件描述符:
在linux下用文件描述符来表示设备文件和普通文件。文件描述符是一个整形的数据,所有对文件的操作都是通过文件描述符实现。
文件描述符是文件系统中链接用户空间和内核空间的枢纽。当打开一个或创建一个文件时,内核创建相应的结构,并生成一个整形
的变量传送给用户空间的对应进程。进程用这个文件描述符来对文件进行操作。用户空间的文件操作,例如读、写一个文件时,将文件描述符传送给read 或者 write 。读写函数的系统调用到达内核时,内核解析作为文件描述符的整型变量,找出对应的设备文件,运行相应的函数,并返回用户空间结果。 文件描述符的范围是 0~OPEN_MAX 因此是一个有限的资源,使用完要及时的释放,close函数关闭。文件描述符的值仅在同一个进程中有效,即不同进程的文件描述符,同一个值可能描述的不是同一个文件或者设备。
进程间通信 管道 dup dup2 pipe
一,首先介绍一下两个重要函数,dup, dup2
这两个重要的系统调用可以创建打开的文件描述符的拷贝。
#include <unistd.h>
int dup(int oldfd);
int dup2(int oldfd, int newfd);
二,进程间通信 管道
dup返回最小的未使用的文件描述符, dup的这个规则,现在很容易将管道的文件描述符改为标准输入和标准输出了。
假定当前进程是一个shell,而它调用fork()产生两个子进程,从而创建一个两阶段管道,具体步骤如下:
1,使用pipe() 创建管道。必须首先做到这一点,以便主进程创建的两个子进程能够继承打开的文件描述符。
2,调用fork(),产生我们的“生产者进程”。该进程的标准输出流向管道。在该子进程中进行如下操作:
a, 因为生产者进程不需要管道的读取端所以使用 close(pipefd[0]) 关闭它。
b,使用close(STDOUT_FILENO),(即1)关闭最初的标准输出。
c,使用dup(pipefd[1])将管道的写入端改变为文件描述符1.
d,我们不需要打开的文件描述符的两份拷贝,所以使用close(pipefd[1])关闭一个。
(注意关闭未使用的管道文件描述符的重要性。因为直到最后一个打开的文件描述符关闭,文件才真正关闭,
即使多个进程共享文件描述符时也是如此。因为只有所有的写入端拷贝都被关闭,从管道读取数据 的进程才
会知道是否到达文件末尾,所以关闭未使用的文件描述符至关重要。)
e,调用exec()启动运行的程序。
3,调用fork()产生我们的“消费者进程“,该进程的标准输入来自于管道。该子进程中的和生产者的步骤相似,
具体细节如下:
a,因为右侧的子进程不需要管道的输入端所以使用close(pipefd[1]) 关闭它。
b,使用close(STDIN_FILENO)关闭最初的标准输入。STDIN_FILENO为 0
c,使用dup(pipefd[0])将管道的读取端(输入)改变为文件描述符0.
d,使用close(pipefd[0])关闭文件描述符的一个拷贝。
e,调用exec()启动运行的程序。
4,父进程关闭管道的两端close(pipefd[0]),close(pipefd[1])。
5,最后,在父进程中使用wait等待两个子进程的结束。
源码如下:
/* fork two processes into their own pipeline */
#include <stdio.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
int pipefd[2];
void producer(void);
void consumer(void);
/* main --- fork children, wait for them to finish */
int main(int argc, char **argv)
{
pid_t producer_pid, consumer_pid;
pid_t ret;
int status;
if (pipe(pipefd) < 0) { /* create pipe, very first thing */
perror("pipe");
exit(1);
}
if ((producer_pid = fork()) < 0) { /* fork producer child */
perror("fork");
exit(1);
} else if (producer_pid == 0)
producer();
if ((consumer_pid = fork()) < 0) { /* fork consumer child */
perror("fork");
exit(1);
} else if (consumer_pid == 0)
consumer();
close(pipefd[0]); /* close parent's copy of pipe */
close(pipefd[1]);
while ((ret = wait(& status)) > 0) { /* wait for children */
if (ret == producer_pid)
printf("producer child terminated, status: %x\n", status);
else if (ret == consumer_pid)
printf("consumer child terminated, status: %x\n", status);
else
printf("yow! unknown child %d terminated, status %x\n",
ret, status);
}
return 0;
}
/* producer --- do the work for the left child */
void producer(void)
{
static char *left_argv[] = { "echo", "hi", "there", NULL };
close(pipefd[0]);
close(1);
dup(pipefd[1]);
close(pipefd[1]);
execvp("echo", left_argv);
_exit(errno == ENOENT ? 127 : 126);
}
/* consumer --- do the work for the right child */
void consumer(void)
{
static char *right_argv[] = { "sed", "s/hi/hello/g", NULL };
close(pipefd[1]);
close(0);
dup(pipefd[0]);
close(pipefd[0]);
execvp("sed", right_argv);
_exit(errno == ENOENT ? 127 : 126);
}
程序输出:wang@wang-OptiPlex-320:~/linux_programming$ ./pipeline
+ ./pipelineproducer
child terminated, status: 0
hello
thereconsumer child terminated, status: 0
wang@wang-OptiPlex-320:~/linux_programming$ ./pipeline
+ ./pipeline
hello there
consumer child terminated, status: 0
producer child terminated, status: 0
在编写多进程的代码时,应该小心避免对进程顺序的假定,尤其是调用wait函数系列的代码。
特别的,如果两个通信进程不是来自共同的父进程,那么就不能使用管道机制;而如果两个通信进程之一只能进行标准输入和输出,
那么就不能就不能使用FIFO文件机制。
三,进程管理、 管道总结
1,新进程由fork()创建。调用fork()之后,两个进程运行相同的代码,惟一不同之处在于返回值:子进程0,父为一正整数pid值(为子进程的pid)。
子进程继承了几乎所有父进程的属性拷贝,其中打开的文件是比较重要的。
2,继承的共享文件描述符使得unix语义和优雅的shell控制结构成为可能。由于描述符共享,只有最后打开的文件描述符被关闭,文件才会被正
真关闭。这一点对管道影响尤其大,但它也影响了未链接但仍打开的文件的磁盘块的释放。
3,getpid() 、getppid()调用分别返回当前进程和父进程的ID。父进程死亡的进程会被重设父进程,重设的父进程为特殊的init进程,pid为1。应此,PPID可能会改变,应用程序应该考虑这一点。
4,系统调用nice()(在头文件<unistd.h>)可以调整进程的优先级,允许nice的值范围 -20~19 ,该值越小时 优先级越高。
5,系统调用exec()启动新的程序运行在现存的进程中。注意六个不同版本的用法。
6,atexit()函数注册程序终止时以LIFO顺序运行回调函数。exit(), _exit(), _Exit()函数都会终止程序并将一个退出状态传回给父进程。
7,wait(), waitpid()都是用于恢复子进程退出状态的POSIX函数。
8,管道和FIFO提供了两个进程间的单向通信渠道。管道必须由一个共同祖先设立,而FIFO可以用于任意的两个进程。管道创建由pipe() 而 FIFO 文件由 mkfifo()创建。管道和FIFO对数据进行缓冲,在管道充满或清闲时阻止生产者或消费者。
9,dup() dup2() 创建打开的文件描述符的拷贝。与close()结合,他们可以让管道文件描述符作为管道的标准输入或输出。为了管道正常运行 在exec() 运行目标程序之前,所有未使用的管道各端的拷贝必须关闭。/dev/fd 可以用来创建非线性管道,这一点由Bash shell进程的替代 能力可知。
10,fcntl(),是一个保罗万象的函数,能够完成各种任务。它同时管理文件描述符本身及其底层文件的属性,其几个方面的应用比如:复制文件 描述符 可以模拟dup dup2。获得并设置close-on-exec 标记 此标记是当前文件描述符的仅有属性。获得并设置控制底层文件的标记。
待续....
更多推荐
所有评论(0)