美文网首页
05 进程间通信

05 进程间通信

作者: 夏威夷的芒果 | 来源:发表于2018-11-22 11:25 被阅读6次

    这一章中我们来学习如何使多个进程间有效地通信。由于这些通信方式中很多都基于文件等持久存储,我们将从基本的 I/O 操作讲起,夯实基础后再带你认识管道、信号等多种进程间通信的方法。

    进程间通信

    在前面两章中我们讲完了操作系统中非常重要的一部分内容——内存管理。我们已经提到了,在现代的计算机中,进程的地址空间和物理内存是区别开的,除非一个进程与其它进程共享一段内存,否则不同的进程是不能够使用彼此的地址空间的。一些同学肯定已经想到了,如果不同的进程之间不能共享内存,那么它们互相之间该如何通信呢?比如我写了两个程序 A 和 B,B 需要将 A 的输出值作为输入值运行,那我怎么才能达到这个目的呢?

    在前面的课程中我们已经学习了fork()(,我们可以通过这个系统调用产生一个子进程,然后用 exec()在子进程中执行另一个程序,在父进程中调用wait()等待进程运行完毕,但这样我们在父进程中只能获得进程的退岀码,这之外的数据我们就无法获得了;
    而随着子迸程的终止,子进程的地址空间也会被系统回收,我们就更无法获得它的数据了。为了能够读取这个数据,我们只有把它存储在一个两个进程都能读取、且不会随着进程的终止而被回收的存储位置,这个位置就是外存。

    还记得我们在第一章中提到的文件的抽象吗?文件是对外存中存储的数据的抽象,我们可以利用一个文件进行 进程间的通信(Inter-process Communication,IPC。进程 A 终止前将输出值写入这个文件,然后进程 B 再将这个文件的内容作为输入值读取进来,开始运行。这是进程间通信的一种常见方法;实际上,一段内存的共享也是通过将一个共享的文件同时映射到两个进程的地址空间实现的。

    另一种进程间通信的方法就是 信号(Signal)。信号类似于异常和中断,是异步的;进程在接到信号后在内核态通过对应的信号处理函数来处理该信号。在这一章中我们会先讲解系统对 I/O 和文件的处理,然后再以这些知识为基础去讲解通过文件进行进程间通信的方法,最后再讲到信号。

    基本IO操作

    文件到底是什么?
    我们已经提到,文件是对于外存中存储的数据的抽象,而外存实际上就是磁盘(disk),固态硬盘(SSD,Solid State Drives),磁带(tape)等物理存储设备。这些存储设备与鼠标、键盘、屏幕等无异,都属于 I/O 设备,你可以向这些设备里输入数据,或从这些硬件中获得我们想要的输出数据。因此,从文件中读取数据或向文件中写入数据实际都属于 I/O 的范畴。

    正如我们前面提到的,I/O 设备是多种多样的,如果我们需要针对每个 I/O 设备的特点写一段不同的代码,那工程量之浩大可想而知。好在操作系统给我们提供了非常便捷的抽象层——无论我们想要使用什么 I/O 设备,我们都可以调用同一组系统调用,这就是我们接下来要讲到的基本 I/O 操作。

    我们要讲到的第一个系统调用是open()。顾名思义,它是用来打开一个文件的,在每次读写文件以前都必须调用这个函数打开文件,获得一个代表该文件的文件标识符,然后再对文件标识符进行操作。与你可能见过的fopen()相比,它处于系统中一个更低的抽象层,你可以对文件进行更基础的操作,但也失去了如fgets(), fscanf()这些方便的库函数的帮助。

    #include <sys/types.h>
    #include <sys/stat.h>
    #include <fcntl.h>
    int open(const char* pathname, int flags, mode_t mode);
    

    这个函数使用三个参数:

    • pathname是被打开文件的路径;
    • flags表示的是这次打开文件所需要进行的操作;
    • mode可以被省略,只有在创建一个文件的时候才会被需要,表示的是新建文件的使用权限。

    flagmode都有很多可能的值。这里我们只介绍几个常用的,如果你想了解所有的可能值,你可以到http://linux.die.net/man/ 去看一看。对于这两者你都可以同时同时选择多个值,用或运算连接起来。

    mode表示的是被创建的新文件的使用权限,它有固定的格式:

    • 前三位都是 S_I

    • 从第四位开始表示权限

    • 如果它表示只有一个权限,则第四位为该权限的缩写(R 表示 read,读,W 表示 write,写,X 表示 execute,执行),后三位为权限的对象(USR 表示 user,用户,GRP 表示用户 group,组,OTH 表示 others,其他用户)。

    • 如果它表示三种权限都具备,那么第四位到第六位就是 RWX,最后一位表示权限的对象(U 表示用户,G 表示组,O 表示其他)。

    文件描述与dup

    现在你已经知道怎么调用 open() 了,现在我们就来看看我们能如何利用 open() 返回给我们的 文件描述符(file descriptor)。一个文件描述符就是一个整数,用来代表一个被打开的文件。每个文件描述符都对应一个文件内指针,表示这个被打开文件的实例中指针的位置,也就是说如果一个文件被同一个进程打开
    了多次,那么这几个文件描述符中的指针位置可能是不同的。

    每个系统都对一个进程可以同时打开的文件数量有限制;每个进程都有一个由文件描述符指向文件的 文件描述符表(file descriptor table)。 open() 每次一般会把新打开的文件放到这个表格中的某个空行,然后返回这个文件描述符。需要注意的是,这个文件描述符只是这个进程中代表这个这个文件的描述符,其它进程即使打开同一个文件也可能有不同的文件描述符;只有由 fork() 产生的子进程才会有和父进程一样的文件描述符。

    文件描述符从0 开始,但我们不能使用前三个文件描述符,因为它们是事先被规定好的: 0代表标准输入,1 代表标准输出, 2代表标准错误。这三个文件描述符在进程初始时就已经被打开,你可以通过这些文件描述符从标准输入读取内容,或向标准输出和标准错误写入内容。

    标准输出正是printf()输出的对象,而标准输入就是你在命令行中输入的内容,因此当我们想把一个进程的输出值导入到一个文件里的时候,我们只需要修改 1,2 这两个文件描述符,使他们输出到我们指定的文件中,我们再用其他进程来读取这个文件。能够实现上述功能的就是下面这个系统调用:

    #include <unistd.h>
    int dup2(int oldfd, int newfd);
    

    dup2() 能够使 newfd 指向 oldfd 指向的文件;如果 newfd 本来对应着其他的文件,那么就关掉原来的文件,再使它指向 oldfd 指向的文件。通过调用 dup2() ,我们可以把标准输出和标准错误都关掉,而将 1,2文件描述符替换为我们想要的文件,这样 printf() 的内容就会直接导入到我们想要的文件里。

    但是这样我们就面临着一个问题——如果我想在一段时间后重新向标准输出输出内容,那我该怎么重新把1 设定成标准输出呢?为了解决这个问题,我们需要另一个系统调用:

    #include <unistd.h>
    int dup(int oldfd);
    

    dup() 会选择最小的空闲文件描述符,使它指向 oldfd 指向的文件,它返回的是新的指向这个文件的描述符。这样在调用 dup2() 以前,我们可以先用 dup() 复制一个指向标准输出的文件描述符,然后用 dup2()关闭原来的标准输出的文件描述符。
    在应用 dup() , dup2() 和 open() 时,我们都不能忘掉检查函数调用确实成功——这三个函数在运行产生错误时,会返回 −1。养成良好的习惯可以大大减少你花在“抓虫”上的时间。

    相关文章

      网友评论

          本文标题:05 进程间通信

          本文链接:https://www.haomeiwen.com/subject/niwkqqtx.html