linux进程间通信之管道通信底层实现源码分析

前言

本文所引用的内核代码,来源于linux0.11

什么是管道

进程用户空间是相互独立的,一般而言是不能相互访问的。但很多情况下进程间需要互相通信,来完成系统的某项功能。进程通过与内核及其它进程之间的互相通信来协调它们的行为。管道就是作为进程间的一种通信方式,为啥这种通信方式就做管道呢?

我想这是它的通信特性决定,通俗点来讲,我们来内核中申请一块缓存区,这个缓存区留有两个接口,分别接在两个不同的进程上,这个缓冲区不需要很大,它被设计成为环形的数据结构,以便可以被循环利用,是不是听上去跟管子一样

当管道中没有信息的话,从管道中读取的进程会等待,直到另一端的进程放入信息。当管道被放满信息的时候,尝试放入信息的进程会等待,直到另一端的进程取出信息。当两个进程都终结的时候,管道也自动消失。

管道

管道是如何实现的

前提回要

索引节点,简称为 inode,用来记录文件的元数据,比如 inode 编号、文件大小、访问 权限、修改日期、数据的位置等。索引节点和文件一一对应,它跟文件内容一样,都会 被持久化存储到磁盘中。所以记住,索引节点同样占用磁盘空间, 所以这里我们知道了,索引节点是指向了一块实实在在物理空间

在 Linux 中,管道的实现并没有使用专门的数据结构,而是借助了文件系统的file结构和VFS的索引节点inode。通过将两个 file 结构指向同一个临时的 VFS 索引节点,而这个 VFS 索引节点又指向一块物理空间而实现的。

管道2

底层源码分析

在linux0.11中文件pipe.c文件通过,sys_pipe(), read_pipe() ,write_pipe() 三个函数实现了,管道通信的底层机制,下面我们来看一看,这三个函数是如何实现的

​ **在sys_pipe() 函数中,在 fildes 所指的数组中创建一对文件句柄(描述符)。这对文件句柄指向一管道 i 节点。fildes[0],用于读管道中数据,fildes[1]用于向管道中写入数据。得到的效果图见下: **

 创建管道系统调用函数。
// 在 fildes 所指的数组中创建一对文件句柄(描述符)。这对文件句柄指向一管道 i 节点。fildes[0]
// 用于读管道中数据,fildes[1]用于向管道中写入数据。
int sys_pipe(unsigned long * fildes)
{
	struct m_inode * inode;
	struct file * f[2];
	int fd[2];
	int i,j;

	j=0;
	// 从系统文件表中取两个空闲项(引用计数字段为 0 的项),并分别设置引用计数(f_count++)为 1
	for(i=0;j<2 && i<NR_FILE;i++)
		if (!file_table[i].f_count)
			//(f[j++]=i+file_table)->f_count++; //原代码
			  (f[j++]=file_table[i])->f_count++ ; // 我觉得写成这样更好理解
			  
	// 如果只有一个空闲项,则释放该项(引用计数复位)。有可能一个空闲项都没有,所以判断一下还是有必要
	if (j==1)
		f[0]->f_count=0;
	//得到不到两个空闲项,则返回
	if (j<2)
		return -1;
	
	j=0;
	// 针对上面取得的两个文件结构项,分别分配一文件句柄,并使进程的文件结构指针分别指向这两个
   // 文件结构
	for(i=0;j<2 && i<NR_OPEN;i++)
		if (!current->filp[i]) {
			current->filp[ fd[j]=i ] = f[j];
			j++;
		}
		// 如果只有一个空闲文件句柄,则释放该句柄。
	if (j==1)
		current->filp[fd[0]]=NULL;
	// 如果没有找到两个空闲句柄,则释放上面获取的两个文件结构项(复位引用计数值),并返回-1。
	if (j<2) {
		f[0]->f_count=f[1]->f_count=0;
		return -1;
	}

	// 申请管道 i 节点,并为管道分配缓冲区(1 页内存)。如果不成功,则相应释放两个文件句柄和文
// 件结构项,并返回-1。
	if (!(inode=get_pipe_inode())) {
		current->filp[fd[0]] =
			current->filp[fd[1]] = NULL;
		f[0]->f_count = f[1]->f_count = 0;
		return -1;
	}
	f[0]->f_inode = f[1]->f_inode = inode;
	f[0]->f_pos = f[1]->f_pos = 0;
	f[0]->f_mode = 1;		/* read */
	f[1]->f_mode = 2;		/* write */

	// 将文件句柄数组复制到对应的用户数组中,并返回 0,退出。
	put_fs_long(fd[0],0+fildes);
	put_fs_long(fd[1],1+fildes);
	return 0;
}

管道3

关于管道数据的读写,其实跟文件数据的读写类似,管道写函数通过将字节复制到 VFS 索引节点指向的物理内存而写入数据,而管道读函数则通过复制物理内存中的字节而读出数据。不同的是,在读或者写之前,会进行一些条件的判断,如管道中是否有数据,有多少数据。其次是利用一定的机制同步对管道的访问,在读进程进行时,写进程进入等待状态。读进程完事后,会唤醒写进程,反之亦然。

// 参数 inode 是管道对应的 i 节点,buf 是数据缓冲区指针,count 是读取的字节数
int read_pipe(struct m_inode * inode, char * buf, int count)
{
	int chars, size, read = 0;
	while (count>0) {// 若欲读取的字节计数值 count 大于 0,则循环执行以下操作。
		while (!(size=PIPE_SIZE(*inode))) {// 若当前管道中没有数据(size=0),则唤醒等待该节点的进程,如果已没有写管道者,则返回已读
												// 字节数,退出。否则在该 i 节点上睡眠,等待信息。
			wake_up(&inode->i_wait);
			if (inode->i_count != 2) /* are there any writers? */
				return read;
			sleep_on(&inode->i_wait);
		}

		
		// 取管道尾到缓冲区末端的字节数 chars。如果其大于还需要读取的字节数 count,则令其等于 count。
       // 如果 chars 大于当前管道中含有数据的长度 size,则令其等于 size。
		chars = PAGE_SIZE-PIPE_TAIL(*inode);
		if (chars > count)
			chars = count;
		if (chars > size)
			chars = size;
		count -= chars;// 读字节计数减去此次可读的字节数 chars,并累加已读字节数
		read += chars;
		// 令 size 指向管道尾部,调整当前管道尾指针(前移 chars 字节)。	
		size = PIPE_TAIL(*inode);
		PIPE_TAIL(*inode) += chars;
		PIPE_TAIL(*inode) &= (PAGE_SIZE-1);
		while (chars-->0)
			put_fs_byte(((char *)inode->i_size)[size++],buf++);
	}
	// 唤醒等待该管道 i 节点的进程,并返回读取的字节数。
	wake_up(&inode->i_wait);
	return read;
}




 管道写操作函数。
// 参数 inode 是管道对应的 i 节点,buf 是数据缓冲区指针,count 是将写入管道的字节数。
int write_pipe(struct m_inode * inode, char * buf, int count)
{
	int chars, size, written = 0;
	// 若将写入的字节计数值 count 还大于 0,则循环执行以下操作。
	while (count>0) {
									// 若当前管道中没有已经满了(size=0),则唤醒等待该节点的进程,如果已没有读管道者,则向进程
									// 发送 SIGPIPE 信号,并返回已写入的字节数并退出。若写入 0 字节,则返回-1。否则在该 i 节点上
									// 睡眠,等待管道腾出空间。
		while (!(size=(PAGE_SIZE-1)-PIPE_SIZE(*inode))) {
			wake_up(&inode->i_wait);
			if (inode->i_count != 2) { /* no readers */
				current->signal |= (1<<(SIGPIPE-1));
				return written?written:-1;
			}
			sleep_on(&inode->i_wait);
		}

		// 取管道头部到缓冲区末端空间字节数 chars。如果其大于还需要写入的字节数 count,则令其等于
		// count。如果 chars 大于当前管道中空闲空间长度 size,则令其等于 size。
		chars = PAGE_SIZE-PIPE_HEAD(*inode);
		if (chars > count)
			chars = count;
		if (chars > size)
			chars = size;
		// 写入字节计数减去此次可写入的字节数 chars,并累加已写字节数到 written
		count -= chars;
		written += chars;
		// 令 size 指向管道数据头部,调整当前管道数据头部指针(前移 chars 字节)。
		size = PIPE_HEAD(*inode);
		PIPE_HEAD(*inode) += chars;
		PIPE_HEAD(*inode) &= (PAGE_SIZE-1);
		// 从用户缓冲区复制 chars 个字节到管道中。对于管道 i 节点,其 i_size 字段中是管道缓冲块指针。
		while (chars-->0)
			((char *)inode->i_size)[size++]=get_fs_byte(buf++);
	}
	// 唤醒等待该 i 节点的进程,返回已写入的字节数,退出。
	wake_up(&inode->i_wait);
	return written;
}


以上我们了解到了管道的底层实现原理,但令人疑惑的事,不是说管道是两个进程分别对一块缓存区进行读写么,从上面的代码来看只实现了当前进程对于管道的读写呀?

事实上,管道利用fork机制建立,从而让两个进程可以连接到同一个node上。最开始的时候,上面的两个箭头都连接在同一个进程Process 1上(连接在Process 1上的两个箭头)。当fork复制进程的时候,会将这两个连接也复制到新的进程(Process 2)。随后,每个进程关闭自己不需要的一个连接 (两个黑色的箭头被关闭; Process 1关闭从PIPE来的输入连接,Process 2关闭输出到PIPE的连接),这样,剩下的红色连接就构成了如下图所示。

管道4

让我们来看看具体实现吧

int main(void)
{
    int n;
    int fd[2];
    pid_t pid;
    char line[MAXLINE];
    if(pipe(fd)  0){                 /* 先建立管道得到一对文件描述符 */
        exit(0);
    }

    if((pid = fork())  0)            /* 父进程把文件描述符复制给子进程 */
        exit(1);
    else if(pid > 0){                /* 父进程写 */
        close(fd[0]);                /* 关闭读描述符 */
        write(fd[1], "\nhello world\n", 14);
    }

    else{                            /* 子进程读 */
        close(fd[1]);                /* 关闭写端 */
        n = read(fd[0], line, MAXLINE);
        write(STDOUT_FILENO, line, n);
    }

    exit(0);
}

这里小伙伴又要问了,按这个说法那管道是不是只能用于父子进程,或者兄弟进程?

这里就要引出管道的分类了,管道分为两种 ,有名管道和无名管道 ,在早期版本中只实现无名管道,后面为了实现不相关进程之间的通信,才引入了有名管道机制FIFO,又叫做命名管道(named PIPE)。

FIFO (First in, First out)为一种特殊的文件类型,它在文件系统中有对应的路径。当一个进程以读®的方式打开该文件,而另一个进程以写(w)的方式打开该文件,那么内核就会在这两个进程之间建立管道,所以FIFO实际上也由内核管理,不与硬盘打交道。之所以叫FIFO,是因为管道本质上是一个先进先出的队列数据结构,最早放入的数据被最先读出来,从而保证信息交流的顺序。FIFO只是借用了文件系统(file system,命名管道是一种特殊类型的文件,因为Linux中所有事物都是文件,它在文件系统中以文件名的形式存在。)来为管道命名。写模式的进程向FIFO文件中写入,而读模式的进程从FIFO文件中读出。当删除FIFO文件时,管道连接也随之消失。FIFO的好处在于我们可以通过文件的路径来识别管道,从而让没有亲缘关系的进程之间建立连接

是因为管道本质上是一个先进先出的队列数据结构,最早放入的数据被最先读出来,从而保证信息交流的顺序。FIFO只是借用了文件系统(file system,命名管道是一种特殊类型的文件,因为Linux中所有事物都是文件,它在文件系统中以文件名的形式存在。)来为管道命名。写模式的进程向FIFO文件中写入,而读模式的进程从FIFO文件中读出。当删除FIFO文件时,管道连接也随之消失。FIFO的好处在于我们可以通过文件的路径来识别管道,从而让没有亲缘关系的进程之间建立连接**

这次关于管道的分析就到这了,因为笔者暂时只对低版本的源码进行了阅读,所以这里只引入了有名管道的概率,并未对其进行展开,以后有时间了再补上

  • 4
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
Linux管道是一种进程间通信方式,可以用于实现一个进程将数据传递给另一个进程。它是一种半双工的通信方式,即数据只能在一个方向上流动,而且只能在建立了父子进程关系的进程之间使用。 下面是使用管道实现进程间通信的基本步骤: 1. 创建管道 首先需要使用系统调用pipe()创建一个管道。这个函数会返回两个文件描述符,一个用于读取管道数据,一个用于写入管道数据。 2. 创建子进程 接下来需要使用系统调用fork()创建一个子进程。这个函数会返回两次,一次在父进程中返回子进程的PID,另一次在子进程中返回0。 3. 父进程写入数据 在父进程中,可以通过写入管道文件描述符来将数据传递给子进程。可以使用系统调用write()将数据写入管道。 4. 子进程读取数据 在子进程中,可以通过读取管道文件描述符来获取父进程传递的数据。可以使用系统调用read()从管道中读取数据。 5. 关闭管道 当通信结束后,需要关闭管道。可以使用系统调用close()关闭管道的读取和写入端。 下面是一个简单的代码示例,演示了如何使用管道实现进程间通信: ``` #include <stdio.h> #include <unistd.h> int main() { int fd[2]; pid_t pid; char buf[256]; // 创建管道 if (pipe(fd) < 0) { fprintf(stderr, "pipe error\n"); return -1; } // 创建子进程 if ((pid = fork()) < 0) { fprintf(stderr, "fork error\n"); return -1; } else if (pid > 0) { // 父进程写入数据 close(fd[0]); // 关闭读取端 write(fd[1], "hello world\n", 12); close(fd[1]); // 关闭写入端 } else { // 子进程读取数据 close(fd[1]); // 关闭写入端 read(fd[0], buf, sizeof(buf)); printf("received data from parent: %s", buf); close(fd[0]); // 关闭读取端 } return 0; } ``` 在这个示例中,我们首先创建了一个管道,然后使用fork()创建了一个子进程。在父进程中,我们通过write()将数据写入管道中;在子进程中,我们通过read()从管道中读取数据。最后,我们分别关闭了管道的读取和写入端。 注意,管道的缓冲区大小是有限的,如果写入的数据超过了缓冲区的大小,写入操作会被阻塞,直到有足够的空间。同样地,如果读取的数据为空,读取操作也会被阻塞,直到有数据可读取。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值