当前位置: 首页 > 科技观察

Linux进程、线程、文件描述符的底层原理

时间:2023-03-13 13:20:29 科技观察

说到进程,恐怕面试中问的最多的就是线程和进程的关系,那么我先说说答案:在Linux系统中,进程和线程之间几乎没有区别。Linux中的进程实际上是一种数据结构。顺便了解一下文件描述符、重定向、管道命令的底层工作原理。最后,我们将从操作系统的角度来看,为什么线程和进程基本没有区别。1、什么是进程首先抽象的说,我们的电脑就是这个东西:这个大矩形代表电脑的内存空间,小矩形代表进程,左下角的圆圈代表磁盘,右下角的图代表一些输入输出设备,比如鼠标键盘显示器等。另外,注意内存空间分为两部分,上半部分代表用户空间,下半部分代表内核空间。用户空间包含用户进程需要使用的资源。比如你在程序代码中开启了一个数组,这个数组必须存在于用户空间;内核空间存放内核进程需要加载的系统资源。这些资源一般是不允许用户访问的。但是要注意,有些用户进程会共享一些内核空间资源,比如一些动态链接库等。我们用C语言写一个hello程序,编译得到可执行文件,在命令行运行,打印出一句helloworld,然后程序退出。在操作系统级别,创建了一个新进程。这个进程将我们编译好的可执行文件读入内存空间,执行,最后退出。你编译出来的可执行程序只是一个文件,不是进程。可执行文件必须加载到内存中并打包成进程才能真正运行。进程由操作系统创建。每个进程都有其固有的属性,如进程ID(PID)、进程状态、打开的文件等。创建进程后,将其读入你的程序中,你的程序就会被系统执行。那么,操作系统是如何创建进程的呢?对于操作系统来说,一个进程就是一个数据结构。我们直接看linux的源码:structtask_struct{//processstatelongstate;//虚拟内存结构structmm_struct*mm;//进程号pid_tpid;//指向父进程的指针structtask_struct*parent;//子进程列表structlist_headchildren;//存放文件系统信息的指针structfs_struct*fs;//包含打开进程的数组文件指针structfiles_struct*files;};task_struct是Linux内核对进程的描述,也可以称为“进程描述符”。源码比较复杂,这里截取了一小部分比较常用的。我们主要讲mm指针和files指针。mm指向进程的虚拟内存,这是加载资源和可执行文件的地方;文件指针指向一个数组,该数组包含指向进程打开的所有文件的指针。2.什么是文件描述符?先说文件,就是一个文件指针数组。通常,进程从files[0]读取输入,将输出写入files[1],并将错误消息写入files[2]。例如,从我们的角度来看,C语言的printf函数是将字符打印到命令行,但是从进程的角度来看,它是将数据写入文件[1];]从这个文件中读取数据。每个进程创建时,文件的前三位填充默认值,分别指向标准输入流、标准输出流和标准错误流。我们常说的“文件描述符”就是指这个文件指针数组的索引,所以程序的文件描述符默认0为输入,1为输出,2为错误。我们可以画一个新的图:对于一般的计算机来说,输入流就是键盘,输出流就是显示器,错误流也是显示器,所以现在进程和内核是通过三根线连接起来的。因为硬件是由内核管理的,所以我们的进程需要让内核进程通过“系统调用”来访问硬件资源。PS:不要忘了,linux中的一切都被抽象成文件,设备也是文件,可以读写。如果我们写的程序需要其他资源,比如打开一个文件进行读写,也很简单。做一个系统调用,让内核打开文件,文件会放在files的第四个位置,对应文件描述符3:明白了这个原理,输入重定向就容易理解了。当程序要读取数据时,会去files[0]去读取,所以我们只需要将files[0]指向一个文件,那么程序就会从readdatafromthisfile开始,而不是从键盘读取数据:同理,输出重定向就是将files[1]指向一个文件,那么程序的输出不会写到显示器上,而是写到这个文件中:error重定向也是一样,不再赘述这里。其实管道符号也是差不多这个目的。它将一个进程的输出流和另一个进程的输入流连接成一个“管道”,数据在其中传输。不得不说,这个设计思路真的很巧妙:在这里,你可能也可以看出“Linux中的一切都是文件”的设计思路是高明的。无论是设备、另一个进程、socket套接字还是真实的文件,都可以读写,统一加载到一个简单的files数组中,进程通过一个简单的文件描述符访问相应的资源,而具体细节交给操作系统,有效解耦,美观高效。3、什么是线程?首先必须明确,多进程和多线程都是并发的,都可以提高处理器的利用效率。所以现在的关键是多线程和多进程有什么区别。为什么在linux中线程和进程基本没有区别,因为从linux内核的角度来看,线程和进程并没有区别对待。我们知道系统调用fork()可以创建一个新的子进程,函数pthread()可以创建一个新的线程。但是无论是线程还是进程,都是用task_struct结构来表示的,唯一的区别就是共享数据区不同。也就是说,线程看起来和进程没什么区别,只是线程的一些数据区是和父进程共享的,而子进程是副本,不是共享的。例如,mm结构和files结构在线程之间共享。我画两张图你就明白了:那么,我们的多线程程序就必须使用锁机制来防止多个线程同时写入同一个区域。数据,否则可能造成数据混乱。那么你可能会问,既然进程和线程是相似的,而且多进程不共享数据,即不存在数据混淆的问题,为什么多线程的使用比多进程普遍得多呢?因为在现实中,数据共享的并发是比较常见的。比如十个人同时从一个账户里取十块钱。我们希望的是这个共享账户的余额正确的减少一百元,而不是每人得到一个账户副本,每复制一个账户就减少十元。当然,必须要说明的是,只有Linux系统把线程当成共享数据的进程,并没有特殊对待。许多其他操作系统以不同方式对待线程和进程。线程有自己独特的数据结构。个人觉得这样的设计没有linux那么简单,增加了系统的复杂度。Linux中创建新线程和进程的效率非常高。对于创建新进程时复制内存区域的问题,Linux采用了copy-on-write优化的策略,即不真正复制父进程的内存空间,而是等到一个write复制前需要操作。因此,在Linux中创建新的进程和线程是非常快的。