乐趣区

关于算法:Linux的进程线程文件描述符是什么

———–

说到过程,恐怕面试中最常见的问题就是线程和过程的关系了,那么先说一下答案:在 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 中新建过程和新建线程都是很迅速的

退出移动版