首先介绍下文件描述符:

  在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 标记  此标记是当前文件描述符的仅有属性。获得并设置控制底层文件的标记。




待续....

 

Logo

更多推荐