进程间通信和文件I\O

在Unix进程间通信中,大致有

1. 管道                                     pipe(),用于父子进程间通信(不考虑传递描述符)
2. FIFO(有名管道)       非父子进程也能使用,以文件打通
3. 文件                                     文件操作,效率可想而知
4. 本地套接字                       最稳定,也最复杂.套接字采用Unix域
5. 共享内存                            传递最快,消耗最小,传递数据过程不涉及系统调用
6. 信号                                     数据固定且短小

匿名管道(pipe)

         这种管道只能用于存在亲缘关系的进程之间的通信,也就是父子进程或者是兄弟进程之间的通信。从本质上而言,匿名管道可以理解成一种特殊的文件系统,只是这种文件系统与unix下所说的文件系统不同,它不存在于磁盘上,只是存在于计算机的内存之中(和文件系统中的/proc有点类似)。其创建函数如下:

#include<unistd.h>

int pipe(int fd[2]);

管道创建成功之后通过fd返回两个文件描述符fd[0]和fd[1]。fd[0]为读而打开,fd[1]为写而打开,fd[1]的输出恰好是fd[0]的输入。

由于是半双工通信,因此通过管道进行通信的进程之间数据的流动如图1所示,当然通过关闭不同的文件描述符,也可以使数据反向流动。


                                                                         

通过这张图可以看出,数据是单向流动;

下面给出code:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <string.h>

int main(int argc, char const *argv[])
{
	//用于管道的读写
	int fd[2];
	int status;
	//creat pipe
	pipe(fd);
	pid_t pid;
	pid_t ret=fork();
	char buffer[80];
	char fn[12]="i'm a child";
	if (ret==-1)
	{
		perror("fork");
		exit(1);
	}
	else if (ret==0){
		//关闭读端
		close(fd[0]);

		write(fd[1],fn,sizeof(fn));
		//写完之后,关闭写端
		//close(fd[1]);
		sleep(10);
		exit(1);
	}else{
		sleep(3);
		//printf("%d%d\n",getpid(),ret);
		close(fd[1]);		//关闭写端
		//dup2(fd[0],STDOUT_FILENO);
		status=read(fd[0],buffer,16);
		if(status==-1){
			perror("read");
		}
		else if(status==12){
			printf("read done\n");
		}
		status=read(fd[0],buffer,16);
		printf("阻塞了没?\n");
		if(status==-1){
			perror("read");
		}
		else if(status==0){
			printf("no done\n");
		}
		printf("%s\n",buffer);	
		exit(1);
		

	}
	return 0;
}

一,首先,我让父进程关闭写端,子进程先关闭读端,然后写完一段数据后,关闭写端,然后让子进程等待10秒;让父进程一直读到没有数据为止,此时,从终端输出,就可以发现,当写端已经不存在,而读端还在读数据,则读端将会读到没有数据的时候,就直接返回0。并不会阻塞

二,我不关闭子进程的写端,然后让子进程睡了10秒,让父进程一直读到没有数据为止,这个时候,因为写端一直存在,当读端读到没有数据的时候,就会阻塞!


三,我让子进程关闭读端父进程同时关闭读和写,那么子进程会由于在没有读端的情况下,觉得写数据也没有意思了,就会默认终止进程,发出SIGPIPE的信号,

                
//下面是父进程回收子进程时,处理信号

                wid=wait(&status);
		if(WIFEXITED(status)){
			printf("Normal termination with exit status=%d\n",
			WEXITSTATUS(status) );
		}
		if(WIFSIGNALED(status)){
			printf("Killed by signal=%d%s\n",
			WTERMSIG(status),
			WCOREDUMP(status)?"(dumped core)":"");
		}
		if(WIFSTOPPED(status)){
			printf("Stopped by signal=%d\n",
			WSTOPSIG(status));
		}

Killed by signal=13

通过查询 kill -l ,发现13为 SIGPIPE。

 四,如果让父进程读端存在,那么一直让子进程写数据,则当管道满了的时候,就会阻塞管道,等待读端去读数据;


下面,我简单说一下关于共享存储映射(存储I/O映射)和缓冲I\O映射,由于没有看过linux内核设计,但是我看到“普通文件IO需要复制两次,内存映射文件mmap只需要复制一次”这句话的时候,比较迷茫,不知道它大体上是怎么操作的;于是我翻看了书籍和一些比较好的博客,下面做一下总结:

我先把之所以迷茫的点先罗列起来,然后逐个的解释。

一,关于内核空间和用户空间的区别以及作用

二,页缓存

三,普通文件I\O和映射I\O到底是怎么运作的

  1. 关于内核空间和用户空间的区别以及作用

(这里,是在我看到别人总结的时候,提炼的)

首先,说到内核空间和用户空间,就必须说到保护模式

          如果一段程序能够完全控制物理内存,那么它就能做到任意改变计算机的状态,包括干掉整个操作系统然后把自己变成操作系统;把自己变成操作系统的一部分等等。通常来说操作系统肯定是不乐意的了。

如果用户程序自己可以访问大部分的硬件设备;用户程序就可以随意修改属于操作系统的数据。那么随意篡改操作系统,则会使操作系统崩溃。

单任务的情况下已经有不少问题了,到了多任务模式下,问题就更严重了:

  1. 因为多个应用程序要独立加载,如果两个应用程序执意要使用同一个内存地址,那就会发生严重的问题,操作系统必须防止这种事情发生
  2. 外部设备一般来说都是很傻的,它并不知道多任务的存在,不管谁操作外部设备它都是一样响应。这样如果多个应用程序自己直接去操纵硬件设备,就会出现相互冲突,有可能一个程序的数据被发送到了另一个程序等等
  3. 操作系统必须自己响应硬件中断,通过硬件中断来切换任务上下文,让合适的任务在合适的时机继续执行。如果应用程序自己把中断响应程序改掉了,整个操作系统都会崩溃
  4. 操作系统必须有能力在单个应用程序崩溃的情况下清理这个应用程序使用的资源,保证不影响其他应用程序;这就要求它必须清楚知道每个应用程序使用了哪些资源

所以要限制应用程序的行为,必须在应用程序和操作系统执行时有不同的状态,核心问题在于保护关键寄存器和重要的物理内存。

因此必须让CPU区分当前究竟是执行操作系统(开放所有能力)还是应用程序(限制危险功能),从而我们就将CPU执行时,划分成两种不同的状态,保证在应用程序下就不会触及操作系统的数据和代码。从而这个时候就需要操作系统的配合,设置哪些内存可以访问,哪些不能访问(或者说只有操作系统状态下能访问),不能访问的包括操作系统自己的代码区和数据区、中断向量表等。因此从这里,我们就可以看到一点,当在内核模式下,内存还是那些内存,但此时拥有了特权,可以访问更多的数据和代码了;通过这样的特权切换,可以保护硬件设备和操作系统不被破坏。但是我们需要在两种状态下切换,此时就需要CPU触发中断机制,然后进入到操作系统状态,因此应用程序状态不能任意切换到操作系统状态,但也需要有触发进入操作系统代码并切换到操作系统状态的能力(否则无法调用操作系统功能)

因此这里就有了内核态用户态;

注意到,内核态并不是一个东西,没有处于什么地方一说,它是CPU的两种状态之一。因此说切换,其实就是拥有了特权。

从而为了判断哪些内存能访问,和多用户的独立运行,这里就引入了虚拟地址空间,即用户空间使用的都是虚拟地址,通过MMU单元来建立虚拟地址和物理地址的映射,而内核态下,就允许访问所有内存。这里注意内核态下访问内存,并不是用户可以直接访问了,而是通过切换模式,进入到内核态,由操作系统代为处理,当处理完成,则返回用户态。

总结来自https://www.zhihu.com/question/306127044/answer/555327651

         2.页缓存

linux中页缓存的本质就是对于磁盘中的部分数据在内存中保留一定的副本,使得应用程序能够快速的读取到磁盘中相应的数据,并实现不同进程之间的数据共享。因此,linux中页缓存的引入主要是为了解决两类重要的问题:

        1.磁盘读写速度较慢(ms 级别);

        2.实现不同进程之间或者同一进程的前后不同部分之间对于数据的共享

如果没有进程之间的共享机制,那么对于系统中所启动的所有进程在打开文件的时候都要将需要的数据从磁盘加载进物理内存空间,这样不仅造成了加载速度变慢(每次都从磁盘中读取数据),而且造成了物理内存的浪费。为了解决以上问题,linux操作系统使用了缓存机制.

          3.普通文件I\O和内存映射I\O

比如,我现在想从一个datd.txt中读取数据,那么我会调用read系统函数,接下来就是描述一下,是怎么发生的:

        1.进程调用库函数read()向内核发起读文件的请求,因为我是想从磁盘读取,从而用户下并没有这个特权,切换到内核模式下,由操作系统处理

        2.内核通过检查进程的文件描述符定位到虚拟文件系统已经打开的文件列表项,调用该文件系统对VFS的read()调用提供的接口;

        3.通过文件表项链接到目录项模块,根据传入的文件路径在目录项中检索,找到该文件的inode;

        4.inode中,通过文件内容偏移量计算出要读取的页;

        5.通过该inode的i_mapping指针找到对应的address_space页缓存树---基数树,查找对应的页缓存节点;

        (1)如果页缓存节点命中,那么直接返回文件内容;

        (2)如果页缓存缺失,那么产生一个缺页异常,首先创建一个新的空的物理页框,通过该inode找到文件中该页的磁盘地址,读取相应的页填充该页缓存(DMA的方式将数据读取到页缓存),更新页表项;重新进行第5步的查找页缓存的过程;

        6.然后将页缓存中的数据复制到用户空间指定的物理内存中

        7.切换到用户模式下;


我们可以看到在一次read的读取过程中,其发生了两次的数据复制,一次是磁盘到页缓存中,一次是页缓存到用户空间中;

(如果页缓存已经被修改,那么就称它为脏页,那么将由系统操作进行排序,可以进行延迟写回磁盘,或者同步写)

下面这两张图我觉得很好:


 这里在说一下匿名管道;如果我现在创建了匿名管道,现在想将文件A的数据读到管道中,然后将数据传到子进程,并写到文件B中。那么该过程是什么样子的呢?

首先,进程的数据区位于0-3G的虚拟地址空间中,3G-4G为内核区,这里假设,文件A和文件B的部分已经存储在页缓存(内核缓冲区)中

(1)父进程通过系统调用read()从文件A读取数据的过程中,父进程的状态切换到内核态,读取数据并保存到父进程空间中的buf中,再切换回用户态。这里发生了第一次数据的拷贝。
(2)父进程通过系统调用write()将读取的数据从buf中拷贝到管道的过程中,父进程状态切换到内核态,向管道写入数据,再切换回用户态。这里发生第二次数据拷贝。
(3)子进程通过系统调用read()从管道读取数据的过程中,子进程状态切换到内核态,读取数据并保存到子进程空间中的buf中,再切换回用户态。这里发生第三次数据拷贝。
(4)子进程通过系统调用write()将读取的数据从buf中拷贝到文件B的过程中,子进程状态切换到内核态,向文件B写入数据,再切换回用户态。这里发生第四次数据拷贝。
    

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值