说到进程,恐怕面试中最多见的问题就是线程和进程的关系了,那么先说一下答案:在 Linux 系统中,进程和线程几乎没有区别。git
Linux 中的进程就是一个数据结构,看明白就能够理解文件描述符、重定向、管道命令的底层工做原理,最后咱们从操做系统的角度看看为何说线程和进程基本没有区别。面试
PS:我认真写了 100 多篇原创,手把手刷 200 道力扣题目,所有发布在 labuladong的算法小抄,持续更新。建议收藏,按照个人文章顺序刷题,掌握各类算法套路后投再入题海就如鱼得水了。算法
首先,抽象地来讲,咱们的计算机就是这个东西:数组
这个大的矩形表示计算机的内存空间,其中的小矩形表明进程,左下角的圆形表示磁盘,右下角的图形表示一些输入输出设备,好比鼠标键盘显示器等等。另外,注意到内存空间被划分为了两块,上半部分表示用户空间,下半部分表示内核空间。数据结构
用户空间装着用户进程须要使用的资源,好比你在程序代码里开一个数组,这个数组确定存在用户空间;内核空间存放内核进程须要加载的系统资源,这一些资源通常是不容许用户访问的。可是注意有的用户进程会共享一些内核空间的资源,好比一些动态连接库等等。多线程
咱们用 C 语言写一个 hello 程序,编译后获得一个可执行文件,在命令行运行就能够打印出一句 hello world,而后程序退出。在操做系统层面,就是新建了一个进程,这个进程将咱们编译出来的可执行文件读入内存空间,而后执行,最后退出。并发
你编译好的那个可执行程序只是一个文件,不是进程,可执行文件必需要载入内存,包装成一个进程才能真正跑起来。进程是要依靠操做系统建立的,每一个进程都有它的固有属性,好比进程号(PID)、进程状态、打开的文件等等,进程建立好以后,读入你的程序,你的程序才被系统执行。app
那么,操做系统是如何建立进程的呢?对于操做系统,进程就是一个数据结构,咱们直接来看 Linux 的源码:socket
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 内核对于一个进程的描述,也能够称为「进程描述符」。源码比较复杂,我这里就截取了一小部分比较常见的。ide
其中比较有意思的是mm
指针和files
指针。mm
指向的是进程的虚拟内存,也就是载入资源和可执行文件的地方;files
指针指向一个数组,这个数组里装着全部该进程打开的文件的指针。
PS:我认真写了 100 多篇原创,手把手刷 200 道力扣题目,所有发布在 labuladong的算法小抄,持续更新。建议收藏,按照个人文章顺序刷题,掌握各类算法套路后投再入题海就如鱼得水了。
先说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 中新建进程和新建线程都是很迅速的。