日志标签 ‘管道’

pipe和fifo二三事

2012年4月23日

1.管道是什么?

管道是一种只存在于内存的特殊文件,没有磁盘文件与之对应。管道是通过虚拟文件系统pipefs而实现的,pipefs与proc、sysfs等特殊文件系统一样,只存在于内存中。另外,管道只能用于半双工通信。

2.pipe()一个管道意味着什么?

pipe()在pipefs文件系统中创建一个新的索引节点,同时创建两个file对象,一个file对象用于读操作,一个file对象用于写操作。pipe()最终将两个file对象对应的文件描述符返回给用户态进程,也就是向pipe()中传递的fd数组。

3.子进程execv()后是否还能继续共享父进程的管道?

子进程execv()后,不能再继续使用父进程创建的管道,因为子进程当前的上下文已经完全被可执行文件替换。如果要继续使用管道,子进程可以在execv()之前将两个文件描述符重定向到标准输入和输出。

4.描述管道的数据结构与索引节点的关系?

管道虽然是一种特殊文件,它仍然通过VFS框架中的inode来描述。由于VFS要对所有不同的文件进行抽象描述,因此inode只对所有文件的共性进行描述。inode中的i_pipe字段指向pipe_inode_info结构,该结构用于描述管道的特性。

5.写管道时写入的字节量与管道大小的关系?

管道缓冲区通常为一个单独的页框,因此大小默认为4096字节。如果两个或者多个进程并发的写入一个管道,那么任何少于4096字节的写操作都是原子的。但是,如果向管道写入大于管道缓冲区大小的数据,则写操作是可以分割的,也就是说多个进程的写操作可以交叉进行,此时应该注意进程的同步。

6.有名管道是什么?

有名管道是一种设备文件,有对应的磁盘索引节点。因为存在于磁盘上,因此可以被任何进程打开使用。有名管道是一种半双工通信方式。

7.ls | more 的大致执行过程?

在终端执行ls | more时,shell进程fork()出一个进程A用来执行上述命令。A进程调用pipe(),返回文件描述符fd1和fd2,分别用于读和写管道。进程A两次调用fork(),产生两个子进程。进程A关闭fd1和fd2。

对于第一个子进程,它调用dup2(fd2,1)将写文件描述符重定向到标准输出。接下来调用execv()系统调用执行ls程序,该程序将自己的输出写入管道。

对于第二个子进程,它调用dup2(fd1,0)将读文件描述符重定向到标准输入。接下来调用execv()系统调用执行more程序,该程序从管道中读取数据。

shell管道重定向实现

2010年8月24日

如果你实现过my_shell.c,那么对管道重定向应该有印象。但是本文中所述的管道重定向,将采用最近我们学习的管道相关系统调用函数来实现。随后,也将和大家一起再去回顾当初my_shell.c中是如何实现管道重定向的

对于管道符号,这里只做简单的说明:管道符前命令的输出作为管道符后命令的输入。对于一般命令而言,输入均来自标准输入,而输出则至标准输出。但是为了实现管道重定向,我们先创建管道,然后将管道写端重定向到标准输出,将管道读端重定向到标准输入。当然这两次重定向分别在不同的进程中完成。具体代码实现可参考下面的代码片段。

	if(pipe(fd)==-1)
	{
		printf("my_pipe:can't create a pipe\n");
		exit(1);
	}

	pid=fork();
	if(pid==0)
	{
		close(fd[0]);
		dup2(fd[1],STDOUT_FILENO);
		close(fd[1]);

		if(execlp(argv[1],argv[1],NULL)==0)
		{
			printf("my_pipe:can't execute the first command in the child process\n");
			exit(1);
		}
	}
	else if(pid>\0)
	{
		close(fd[1]);
		dup2(fd[0],STDIN_FILENO);
		close(fd[0]);

		if(execlp(argv[2],argv[2],NULL)==0)
		{
			printf("my_pipe:can't execute the second command in the parent process\n");
			exit(1);
		}
	}
	else
	{
		printf("my_pipe:can't create a new process\n");
		exit(1);
	}

上述代码所实现的管道重定向只支持没有参数的命令,比如./my_pipe ls wc(相当于命令ls | wc)。如果想要使用命令参数,可以在次基础上继续完善。

可以发现,管道是作为前后两个命令的数据缓冲区,各命令的相关输入或输出只是“私下”操作。既然理解了上述代码,那么my_shell.c中的管道的实现代码也就一目了然了。

首先,创建一个子进程,然后在子进程中创建子子进程(pid2=fork())。其实无非是让这两个子进程去分别执行管道符前后的命令。这里应该注意的是此段代码是如何“模拟”管道的。我们可以发现,其实并没有真正的pipe一个管道,而是创建了一个实实在在tempfile文件而已,最后使用完毕再悄悄删除次文件。

再看具体是如何重定向的,在子子进程中,以写方式打开”管道”,将tempfile定向到标准输出。在子进程中,以读方式打开”管道”,将tempfile定向到标准输入。

看到这里,你应该已经发现,这里所用到的原理其实和一开始我们所述的方法是一致的。

			if(pid==0)//child_process
			{
				int pid2;
				int status2;
				int fd2;

				if((pid2=fork())<\0)
				{
					printf("fork2 error\n");
					return;
				}
				else if(pid2==0)//child_child_process
				{
					if(!(find_command(arg[0])))
					{
						printf("%s: command not found\n",arg[0]);
						exit(0);
					}
					fd2=open("/tmp/tempfile",O_WRONLY|O_CREAT|O_TRUNC,0644);
					dup2(fd2,1);
					execvp(arg[0],arg);
					exit(0);
				}

				if(waitpid(pid2,&status2,0)==-1)//waiting for child_child_process
				{
					printf("wait for child_child process error\n");
				}

				if(!(find_command(argnext[0])))
				{
					printf("%s:command not found\n",argnext[0]);
					exit(0);
				}

				fd2=open("/tmp/tempfile",O_RDONLY);
				dup2(fd2,0);
				execvp(argnext[0],argnext);

				if(remove("/tmp/tempfile"))
				{
					printf("remove error\n");
				}
				exit(0);
			}

			

IPC under Linux-FIFO(2)

2010年8月23日

下面我们来详细说明有名管道的读写规则。

3.有名管道读取规则

(1)如果进程A事先写打开FIFO,但却不进行写操作(FIFO中为空),那么进程B对其进行读操作时候将会阻塞,或者直接返回-1(当设置了非阻塞标志时)。
我们继续以《LinuxC编程实战》一书中的例子(P252)为原型,对procwrite.c函数稍作改动即可,具体代码如下。

	printf("Now the writer will sleep 5s..\n");
	sleep(5);
	printf("Now the writer wake up and will write to FIFO..\n");
	write(fd,buf,strlen(buf)+1);
	printf("writer will leave!\n");
	close(fd);

我们可以发现,procwrite进程运行后,发生了阻塞,这一点可用上文的打开规则来解释。当在另一个终端运行procread进程后,此时的procwrite正常运行。由于我们让procwrite进程睡眠5s,所以当前管道缓冲区是空的,我们可以在另一终端发现procread进程发生了阻塞。但当procwrite睡眠结束,继续运行时,procread进程可正确读出管道内数据。

如果你理解了规则1,那么我们继续来了解两种读操作阻塞的情况。

(2)当FIFO中有数据时,但是正在被进程A所读取,如果B进程此时也欲读FIFO,B进程将阻塞;如果FIFO中无数据,那么读进程也将阻塞(规则1所述)。想要解除阻塞只要在管道内写入数据即可,不管写入多少数据,也不管读进程请求读多少数据。

我们继续以上述例子为原型,但是需要两个读进程。首先procwrite进程先进行第一次写,然后睡眠20s,接着进行第二次写操作,这是为了解除因读操作而阻塞的进程。

        //procwrite.c部分代码
	printf("Now the writer will write data to FIFO..\n");
	write(fd,buf,strlen(buf)+1);
	printf("writer will leave!\n");
	sleep(20);
	strcpy(buf,"The write write another data now");
	write(fd,buf,strlen(buf)+1);
	close(fd);
	exit(0);

在procread.c程序中,我们让此进程一次性只读取一个字符。

int n=20;
while(n--)
{
read(fd,buf,1);
buf[strlen(buf)]='\0';
printf("Read content:%s\n",buf);
sleep(2);
}
close(fd);
unlink(FIFO_NAME);
exit(0);

在procread2.c程序中,我们让此进程一次性读完管道内所有数据。

printf("I will read data from FIFO..\n");
read(fd,buf,BUF_SIZE);
printf("Read content:%s\n",buf);
close(fd);
exit(0);

我们首先运行procwrite,然后立马运行procread,可以看到其每次只输出一个字符;接着我们运行procread2,可以看到这个进程一次性读完了管道内剩余的所有数据,而此时再看运行procread的那个终端,可知procread进程发生了阻塞,不过再稍等片刻,可发现它有接着输出字符,这是因为procwrite进程第二次的写操作解除了procread进程的阻塞。

这里只是为大家做一个简单的演示,我们可以在理解上述规则的基础上编写其他程序去验证。

4.有名管道的写规则

如果打开FIFO时,没有加非阻塞标志,那么写操作遵循以下的规则:

(1)如果写入数据的字节数不大于PIPE_BUF(注意,从内核2.6.11开始,其值为65536),Linux将保证写操作的原子性。当管道内的剩余缓冲区大于要写入的数据字节数时,那么将一次性写完数据;否则,进程进入睡眠状态,直至管道内的缓冲区可以容纳要写入的数据为止。

(2)如果写入的数据字节数不大于PIPE_BUF,那么Linux将不保证写操作的原子性。管道内一有空闲的缓冲区,写进程就去写数据,直至所有数据都被写完为止。

如果打开FIFO的时候加入了阻塞标志,那么写操作遵循一下规则:

(1)如果要写入数据的字节数大于PIPE_BUF,那么linux将不会保证写入的原子性。在写满所有FIFO空闲缓冲区后,写操作返回。

(2)如果要写入的数据的字节数不大于PIPE_BUF,那么linux将保证写入的原子性。如果当前FIFO空闲缓冲区能够容纳请求写入的字节数,写完后成功返回;如果当前FIFO空闲缓冲不能够容纳请求写入的字节数,则返回EAGAIN错误,提醒以后再写。

IPC under Linux-FIFO(1)

2010年8月20日

上文中我们了解了管道这个基本的通信方式,不过在文章最后我们也总结了管道的相关局限性,比如仅可以在亲缘进程之间进行通信等。本文要说明的IPC通信方式实际上可称作PIPE的加强版:有名管道(FIFO或者named PIPE)。首先“望文思意”,FIFO是具有具体文件名的(具体路径名),因为FIFO是一个设备文件。因此,对于一般文件的操作,比如open,write,read都可以对有名管道进行操作。

1.创建有名管道

使用mkfifo函数就可以了,但是并不会类似PIPE那样:先fork,再mkfifo。还记得吗,任意进程间都可以进行FIFO通信,因此在A程序(进程)中创建了管道,在B程序(进程)中只要打开相应的管道即可。

2.有名管道的打开规则

当我们创建了有名管道时,每次使用前还要用open函数打开这个管道文件。其实这并不是有名管道的什么特殊之处,因为有名管道是存在于磁盘上的文件,而管道是存在内存中的特殊文件。

这里我们需要注意的是,进程以不同的方式打开有名管道,会对其自身产生不同的影响。下面我们一点一点的去了解。

首先,如果进程以O_RDWR方式打开管道,此进程一定不会阻塞。

如果进程A以只写方式打开管道,那么这个进程会一直阻塞到有另外一个进程B以写方式打开管道为止。当然,如果A进程以只读方式打开管道前,B进程就已经以只写方式打开了管道,那么A进程必然不会阻塞。

类似的,如果A进程以只写方式打开管道,而没有其他进程以读方式打开管道,那么A进程也会阻塞。

上面我们所说的规则具有一般性,因为缺省情况下进程是以阻塞方式打开文件的。如果我们打开管道时加入了非阻塞标志(O_NONBLOCK),那么又会产生和上面不一样的情况。具体如下问所述。

如果进程A以只写方式打开管道,此时又无其他进程以只读方式打开管道,那么进程A将返回错误标志ENXIO。进程A以只读方式打开管道的情况与上类似。

如何去验证上述打开规则?《LinuxC编程实战》一书中例10-5就是个很好的验证程序。

如果按照此书所述那样去运行程序,可以发现procwrite会在procread运行前阻塞。如果我们在procwrite.c中以可读写方式打开管道,那么可以发现运行procwrite后立马会运行完毕。而且,我们ls -l一下,可以发现当前目录下多了一个myfifo的管道文件(注意文件存取权限最前面的那个P)。

关于打开规则更多的测试,请参考这里的例子:附录2,对我们的理解有极大帮助。

IPC under Linux-Pipe

2010年8月19日

管道是一种最基本的IPC方式,本文以笔记形式以及典型程序来描述管道的特点。

1.管道特点关键字

  • 半双工通信
  • 亲缘进程间通信
  • 内存中的特殊文件
  • 在末尾写数据,在头部读数据

2.管道创建

先创建管道,再创建进程。即先pipe,再fork。

3.从管道中读数据

  • 管道的写端不存在,那么进程认为已经读到了数据的末尾,读函数返回的字节数为0。
  • 这里有三个名词:PIPE_BUF,请求读取字节数(request_num),管道中数据量(pipe_num);那么关于这三个量之间的关系如下图。

下面这个程序可以加深你对上述读规则的理解,实现代码如下:

int main()
{
	int fd[2];
	int ret,request;
	char rbuf[4096];
	char wbuf[5000];
	char *msg="hello,pipe!",*msg2="hello,I come here ,too!";
	pid_t pid;
	int stat_val;

	if(pipe(fd)\<\0)
	{
		printf("pipe error\n");
		exit(1);
	}

	if((pid=fork())==0)
	{
		//child process write
		close(fd[0]);
		ret=write(fd[1],msg,strlen(msg)+1);
		printf("I am writer and I write %d Byte data\n",ret);
          	sleep(5);
		ret=write(fd[1],msg2,strlen(msg2)+1);
		printf("I am writer and I write %d Byte data\n",ret);
		sleep(5);
		close(fd[1]);
		exit(0);
	}
	else if(pid\>\0)
	{
		//parent process read
		close(fd[1]);
		request=5000;
		ret=read(fd[0],rbuf,request);
		printf("when reader request %d Byte,the actual return Bytes is %d\n",request,ret);
		request=10;
		ret=read(fd[0],rbuf,request);
		printf("when reader requesr %d Byte,the actual return Bytes is %d\n",request,ret);
		request=100;
		ret=read(fd[0],rbuf,request);
		printf("when reader request %d Byte,the actual return Bytes is %d\n",request,ret);
		request=1;
		ret=read(fd[0],rbuf,request);
		printf("when reader request %d Byte,the actual return Bytes is %d\n",request,ret);
		close(fd[0]);
		exit(0);
	}
}

该程序中父进程读数据,子进程去写数据。至于运行过程请你亲自去尝试,因为我觉得我在这里说的多么具体,都不及你亲自去感受。

首先你可能会想:是否应该在父进程所要执行的代码前加一个sleep(1)?因为在父进程读之前,管道内至少得有数据啊!其实可以不用加。因为如果管道内无数据可读,父进程自动阻塞,直至有数据或者不存在写端时才会继续运行。

在子进程第一次写操作后,就睡眠5秒,此时父进程刚好读完管道内的所有数据,因为请求的数据量为5000,而PIPE_BUF为4960。现在管道内已经无数据可读了,因此父进程阻塞。5秒过后,子进程继续写入24Byte。父进程第二次请求10Byte,因此根据上述我们所说的读规则,read函数返回10。父进程第三次请求读100Byte的数据,根据上述读规则,他将返回管道内所有数据的字节数,因此read函数返回14Byte。

父进程第四次请求读前,管道已经空了,按照我们刚所说的,父进程现在应该阻塞了。因此在子进程睡眠5秒后,父进程发现读端已经关闭,因此读函数返回0。

4.从管道中写数据

  • 只有管道读端存在时,管道写数据才有意义。
  • 写数据时,Linux不能保证写入的原子性。

关于写数据的第一条规则,很好去验证,比如上述程序,如果在父进程一开始就关闭两个端,那么子进程将会自动退出。但是如果将子进程的读端不关闭,那么子进程是可以正常写入pipe的。因此,在写管道时,应该至少保证一个进程的的读端是打开的。

关于写数据的非原子性是指,如果要写入A字节的数据,而此时管道内只有B字节的缓冲区,而且A>B。那么此时写端的进程就会分批写入数据:先写入B字节,然后再写入(A-B)字节。特殊的,当A-B仍然大于PIPE_BUF的时候,那么还是先写PIPE_BUF字节,然后再写(A-B-PIPE_BUF)字节的数据。如果当前在写入最后一批数据前,可以一次性写入管道,那么此时写函数将返回写入的字节数,而不是等所有数据都写完毕后再返回写入的字节数。

下面这个程序请务必亲自动手验证。

int main()
{
	int fd[2];
	int ret,request;
	char rbuf[50000];
	char wbuf[65000];
	char wbuf2[100000];
	char *msg="hello,pipe!",*msg2="hello,I come here ,too!";
	pid_t pid;
	int stat_val;

	if(pipe(fd)\<\0)
	{
		printf("pipe error\n");
		exit(1);
	}

	if((pid=fork())==0)
	{
		//child process write
		close(fd[0]);
		ret=write(fd[1],wbuf,65000);
		printf("I am writer and I write %d Byte data\n",ret);
		ret=write(fd[1],wbuf2,100000);
		printf("I am writer and I write %d Byte data\n",ret);
		printf("writer has over\n");
		close(fd[1]);
		exit(0);
	}
	else if(pid\>\0)
	{
		//parent process read
//		sleep(3);
		close(fd[1]);
		while(1)
		{
			sleep(1);
			ret=read(fd[0],rbuf,10000);
			printf("I am reader and the num of reading data is %d\n",ret);
		}
		close(fd[0]);
		exit(0);
	}
}

在程序运行后,当第七次显示已经读了10000字节时,就说明了写管道时的非原子性。因为如果写是原子性的,即便先写入的65000字节被读走,那么也不可能一次性全部写入100000字节(因为PIPE_BUF为65536)。当第七次提示已读入10000字节时,说明先将一部分数据写入了管道内的空闲区,否则第七次读入提示只显示已读入5000字节。 当第11次读入提示显示后,就会接着显示已写入100000字节的提示。因为前11次读中:先用7次(第七次的10000字节中既有第一次写入的5000字节,又有第二次的一部分数据)读走了第一次写入的65000字节,然后用5次读走第二次要写入100000字节的前45000字节,当剩余65000字节时,已经可以一次性写入管道,因此在最后一次写之前返回写入的字节数100000。这与上述我们概述的原理符合。

以上解释需要多次实验和斟酌。

5.管道的局限性关键词

  • 单向流动
  • 缓冲区局限性
  • 亲缘进程间通信局限性
  • 字符流传送

对于管道半双工流动的特点,如果使用两条管道,那么就可以实现两个进程间全双工通信。具体例子《LinuxC编程实战》一书中有详解。不过我这里想特别说明的是,由于管道的读端在无数据可读时会自动阻塞,因此如果两个管道都首先进行读数据,那么此程序会发生死锁,因为父子进程都在等待数据的出现(当然它们等待的管道不同)。因此解决的办法是父子进程都先去写数据或和一个先读一个先写。

对于管道的另一个应用,就是父进程创建子进程后向子进程传递参数(具体可见《实战》P250)。对于这个应用,下面仅说明如何传递。运行监控端程序时,会输入相应的参数(argv[1]),父进程将其写入管道。按照以往的读管道规则,子进程直接从管道读就可以了,但是在本应用中,子程序会去执行一个新的程序(ctrlpocess.c)。我们知道子进程在执行exec函数后,它的“前身”除了pid,几乎一点都不留。所以此刻在新进程中我们直接使用fd[0],fd[1]显然不行。

现在该如何解决?我们既要从管道中读数据,还得保证有一个可用的文件描述符。显然我们应该在子进程“离别”前(执行exec前)将读端(fd[0])定向到标准输入端。这样子进程在执行新的程序时就可以在标准出入端读数据了。

最后需要说明的是,本文仅仅阐述了管道部分的基本知识点,要深入理解管道则需要多次实践。毕竟实践出真知。

windows 7 ultimate product key

windows 7 ultimate product key

winrar download free

winrar download free

winzip registration code

winzip registration code

winzip free download

winzip free download

winzip activation code

winzip activation code

windows 7 key generator

windows 7 key generator

winzip freeware

winzip freeware

winzip free download full version

winzip free download full version

free winrar download

free winrar download

free winrar

free winrar

windows 7 crack

windows 7 crack

windows xp product key

windows xp product key

windows 7 activation crack

windows7 activation crack

free winzip

free winzip

winrar free download

winrar free download

winrar free

winrar free

download winrar free

download winrar free

windows 7 product key

windows 7 product key