设为首页 | 收藏本站欢迎来到!

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

作者:admin      来源:admin      发布时间:2020-10-26

提到进程,恐怕面试中最常见的问题便是线程和进程的关系了,那么先说一下答案: 在 Linux 体系中,进程和线程几乎没有差异 。

Linux 中的进程其实便是一个数据结构,顺带能够了解文件描绘符、重定向、管道指令的底层作业原理,最终咱们从操作体系的视点看看为什么说线程和进程根本没有差异。

首要,笼统地来说,咱们的计算机便是这个东西:

这个大的矩形表明计算机的 内存空间 ,其间的小矩形代表 进程 ,左下角的圆形表明 磁盘 ,右下角的图形表明一些 输入输出设备 ,比方鼠标键盘显示器等等。别的,留意到内存空间被区分为了两块,上半部分表明 用户空间 ,下半部分表明 内核空间 。

用户空间装着用户进程需求运用的资源,比方你在程序代码里开一个数组,这个数组必定存在用户空间;内核空间寄存内核进程需求加载的体系资源,这一些资源一般是不允许用户拜访的。可是留意有的用户进程会同享一些内核空间的资源,比方一些动态链接库等等。

咱们用 C 言语写一个 hello 程序,编译后得到一个可履行文件,在指令行运转就能够打印出一句 hello world,然后程序退出。在操作体系层面,便是新建了一个进程,这个进程将咱们编译出来的可履行文件读入内存空间,然后履行,最终退出。

你编译好的那个可履行程序仅仅一个文件,不是进程,可履行文件必需求载入内存,包装成一个进程才干真实跑起来。进程是要依托操作体系创立的,每个进程都有它的固有特点,比方进程号、进程状况、翻开的文件等等,进程创立好之后,读入你的程序,你的程序才被体系履行。

那么,操作体系是怎么创立进程的呢? 关于操作体系,进程便是一个数据结构 ,咱们直接来看 Linux 的源码:

struct task_struct {
 // 进程状况
 long state;
 // 虚拟内存结构体
 struct mm_struct *mm;
 // 进程号
 pid_t pid;
 // 指向父进程的指针
 struct task_struct *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 个方位,对应文件描绘符 3:

理解了这个原理, 输入重定向 就很好了解了,程序想读取数据的时分就会去 files[0] 读取,所以咱们只要把 files[0] 指向一个文件,那么程序就会从这个文件中读取数据,而不是从键盘:

同理, 输出重定向 便是把 files[1] 指向一个文件,那么程序的输出就不会写入到显示器,而是写入到这个文件中:

过错重定向也是相同的,就不再赘述。

管道符其实也是殊途同归,把一个进程的输出流和另一个进程的输入流接起一条「管道」,数据就在其间传递,不得不说这种规划思维真的很奇妙:

到这儿,你或许也看出「Linux 中一切皆文件」规划思路的高明晰,不管是设备、另一个进程、socket 套接字仍是真实的文件,悉数都能够读写,一致装进一个简略的 files 数组,进程经过简略的文件描绘符拜访相应资源,详细细节交于操作体系,有用解耦,美丽高效。

首要要清晰的是,多进程和多线程都是并发,都能够进步处理器的运用功率,所以现在的关键是,多线程和多进程有啥差异。

为什么说 Linux 中线程和进程根本没有差异呢,由于从 Linux 内核的视点来看,并没有把线程和进程差异对待。

咱们知道体系调用 fork 能够新建一个子进程,函数 pthread 能够新建一个线程。 但不管线程仍是进程,都是用 task_struct 结构表明的,仅有的差异便是同享的数据区域不同 。

换句话说,线程看起来跟进程没有差异,仅仅线程的某些数据区域和其父进程是同享的,而子进程是仿制副本,而不是同享。就比方说, mm 结构和 files 结构在线程中都是同享的,我画两张图你就理解了:

所以说,咱们的多线程程序要运用锁机制,防止多个线程一起往同一区域写入数据,不然或许形成数据紊乱。

那么你或许问, 已然进程和线程差不多,并且多进程数据不同享,即不存在数据紊乱的问题,为什么多线程的运用比多进程遍及得多呢 ?

由于实际中数据同享的并发更遍及呀,比方十个人一起从一个账户取十元,咱们期望的是这个同享账户的余额正确削减一百元,而不是期望每人取得一个账户的仿制,每个仿制账户削减十元。

当然,必需求阐明的是, 只要 Linux 体系将线程看做同享数据的进程 ,不对其做特别看待 ,其他的许多操作体系是对线程和进程差异对待的,线程有其特有的数据结构,我个人认为不如 Linux 的这种规划简练,增加了体系的复杂度。

在 Linux 中新建线程和进程的功率都是很高的,关于新建进程时内存区域仿制的问题,Linux 采用了 copy-on-write 的战略优化,也便是并不真实仿制父进程的内存空间,而是比及需求写操作时才去仿制。 所以 Linux 中新建进程和新建线程都是很敏捷的 。

以上便是悉数内容,假如有协助的话,无妨点个在看,我看看操作体系相关的文章阅览数据怎么样,不错的话今后能够再写写操作体系方面的小常识。

介绍 Linux 文件体系

有人经过 Redis 攻陷了我的服务器...

Union-Find 并查集算法详解

递归思维:用锅铲给烧饼排序