———–
说到过程,恐怕面试中最常见的问题就是线程和过程的关系了,那么先说一下答案:在 Linux 零碎中,过程和线程简直没有区别。
Linux 中的过程就是一个数据结构,看明确就能够了解文件描述符、重定向、管道命令的底层工作原理,最初咱们从操作系统的角度看看为什么说线程和过程根本没有区别。
一、过程是什么
首先,抽象地来说,咱们的计算机就是这个货色:
这个大的矩形示意计算机的 内存空间 ,其中的小矩形代表 过程 ,左下角的圆形示意 磁盘 ,右下角的图形示意一些 输入输出设施 ,比方鼠标键盘显示器等等。另外,留神到内存空间被划分为了两块,上半局部示意 用户空间 ,下半局部示意 内核空间。
用户空间装着用户过程须要应用的资源,比方你在程序代码里开一个数组,这个数组必定存在用户空间;内核空间寄存内核过程须要加载的系统资源,这一些资源个别是不容许用户拜访的。然而留神有的用户过程会共享一些内核空间的资源,比方一些动态链接库等等。
咱们用 C 语言写一个 hello 程序,编译后失去一个可执行文件,在命令行运行就能够打印出一句 hello world,而后程序退出。在操作系统层面,就是新建了一个过程,这个过程将咱们编译进去的可执行文件读入内存空间,而后执行,最初退出。
你编译好的那个可执行程序只是一个文件,不是过程,可执行文件必须要载入内存,包装成一个过程能力真正跑起来。过程是要依附操作系统创立的,每个过程都有它的固有属性,比方过程号(PID)、过程状态、关上的文件等等,过程创立好之后,读入你的程序,你的程序才被零碎执行。
那么,操作系统是如何创立过程的呢?对于操作系统,过程就是一个数据结构,咱们间接来看 Linux 的源码:
struct task_struct {
// 过程状态
long state;
// 虚拟内存构造体
struct mm_struct *mm;
// 过程号
pid_t pid;
// 指向父过程的指针
struct task_struct __rcu *parent;
// 子过程列表
struct list_head children;
// 寄存文件系统信息的指针
struct fs_struct *fs;
// 一个数组,蕴含该过程关上的文件指针
struct files_struct *files;
};
task_struct
就是 Linux 内核对于一个过程的形容,也能够称为「过程描述符」。源码比较复杂,我这里就截取了一小部分比拟常见的。
其中比拟有意思的是 mm
指针和 files
指针。mm
指向的是过程的虚拟内存,也就是载入资源和可执行文件的中央;files
指针指向一个数组,这个数组里装着所有该过程关上的文件的指针。
二、文件描述符是什么
先说 files
,它是一个文件指针数组。一般来说,一个过程会从files[0]
读取输出,将输入写入files[1]
,将错误信息写入files[2]
。
举个例子,以咱们的角度 C 语言的 printf
函数是向命令行打印字符,然而从过程的角度来看,就是向 files[1]
写入数据;同理,scanf
函数就是过程试图从 files[0]
这个文件中读取数据。
每个过程被创立时,files
的前三位被填入默认值,别离指向规范输出流、规范输入流、规范谬误流。咱们常说的「文件描述符」就是指这个文件指针数组的索引,所以程序的文件描述符默认状况下 0 是输出,1 是输入,2 是谬误。
咱们能够从新画一幅图:
对于个别的计算机,输出流是键盘,输入流是显示器,谬误流也是显示器,所以当初这个过程和内核连了三根线。因为硬件都是由内核治理的,咱们的过程须要通过「零碎调用」让内核过程拜访硬件资源。
PS:不要忘了,Linux 中所有都被形象成文件,设施也是文件,能够进行读和写。
如果咱们写的程序须要其余资源,比方关上一个文件进行读写,这也很简略,进行零碎调用,让内核把文件关上,这个文件就会被放到 files
的第 4 个地位:
明确了这个原理,输出重定向 就很好了解了,程序想读取数据的时候就会去 files[0]
读取,所以咱们只有把 files[0]
指向一个文件,那么程序就会从这个文件中读取数据,而不是从键盘:
$ command < file.txt
同理,输入重定向 就是把 files[1]
指向一个文件,那么程序的输入就不会写入到显示器,而是写入到这个文件中:
$ command > file.txt
谬误重定向也是一样的,就不再赘述。
管道符 其实也是殊途同归,把一个过程的输入流和另一个过程的输出流接起一条「管道」,数据就在其中传递,不得不说这种设计思维真的很柔美:
$ cmd1 | cmd2 | cmd3
到这里,你可能也看出「Linux 中所有皆文件」设计思路的高超了,不论是设施、另一个过程、socket 套接字还是真正的文件,全副都能够读写,对立装进一个简略的 files
数组,过程通过简略的文件描述符拜访相应资源,具体细节交于操作系统,无效解耦,柔美高效。
三、线程是什么
首先要明确的是,多过程和多线程都是并发,都能够进步处理器的利用效率,所以当初的要害是,多线程和多过程有啥区别。
为什么说 Linux 中线程和过程根本没有区别呢,因为从 Linux 内核的角度来看,并没有把线程和过程区别对待。
咱们晓得零碎调用 fork()
能够新建一个子过程,函数 pthread()
能够新建一个线程。但无论线程还是过程,都是用 task_struct
构造示意的,惟一的区别就是共享的数据区域不同。
换句话说,线程看起来跟过程没有区别,只是线程的某些数据区域和其父过程是共享的,而子过程是拷贝正本,而不是共享。就比如说,mm
构造和 files
构造在线程中都是共享的,我画两张图你就明确了:
所以说,咱们的多线程程序要利用锁机制,防止多个线程同时往同一区域写入数据,否则可能造成数据错乱。
那么你可能问,既然过程和线程差不多,而且多过程数据不共享,即不存在数据错乱的问题,为什么多线程的应用比多过程广泛得多呢?
因为事实中数据共享的并发更广泛呀,比方十个人同时从一个账户取十元,咱们心愿的是这个共享账户的余额正确缩小一百元,而不是心愿每人取得一个账户的拷贝,每个拷贝账户缩小十元。
当然,必须要阐明的是,只有 Linux 零碎将线程看做共享数据的过程,不对其做非凡对待,其余的很多操作系统是对线程和过程区别对待的,线程有其特有的数据结构,我集体认为不如 Linux 的这种设计简洁,减少了零碎的复杂度。
在 Linux 中新建线程和过程的效率都是很高的,对于新建过程时内存区域拷贝的问题,Linux 采纳了 copy-on-write 的策略优化,也就是并不真正复制父过程的内存空间,而是等到须要写操作时才去复制。所以 Linux 中新建过程和新建线程都是很迅速的。