Linux的进程、线程、⽂件描述符是什么

说到进程,恐怕⾯试中最常⻅的问题就是线程和进程的关系了,那么先说⼀ 下答案:在Linux系统中,进程和线程⼏乎没有区别

Linux中的进程就是⼀个数据结构,看明⽩就可以理解⽂件描述符、重定向、管道命令的底层⼯作原理,最后我们从操作系统的⾓度看看为什么说线程和进程基本没有区别。

进程是什么

⾸先,抽象地来说,我们的计算机就是这个东⻄:

这个⼤的矩形表⽰计算机的内存空间,其中的⼩矩形代表进程,左下⾓的圆 形表⽰磁盘,右下⾓的图形表⽰⼀些输⼊输出设备,⽐如⿏标键盘显⽰器等等。另外,注意到内存空间被划分为了两块,上半部分表⽰⽤户空间,下半部分表⽰内核空间。

⽤户空间装着⽤户进程需要使⽤的资源,⽐如你在程序代码⾥开⼀个数组,这个数组肯定存在⽤户空间;内核空间存放内核进程需要加载的系统资源,这些资源⼀般是不允许⽤户访问的。但是注意有的⽤户进程会共享⼀些内核空间的资源,⽐如⼀些动态链接库等等。

我们⽤C语⾔写⼀个Hello World程序,编译后得到⼀个可执⾏⽂件,在命令⾏运⾏就可以打印出⼀句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]指向⼀个⽂件,那么程序就会从这个⽂件中读取数据,⽽不是从键盘:

同理,输出重定向就是把files[1]指向⼀个⽂件,那么程序的输出就不会 写⼊到显⽰器,⽽是写⼊到这个⽂件中。错误重定向也是⼀样的,就不再赘述。

管道符其实也是异曲同⼯,把⼀个进程的输出流和另⼀个进程的输⼊流接起 ⼀条「管道」,数据就在其中传递,不得不说这种设计思想真的很优美。

到这⾥,你可能也看出「Linux 中⼀切皆⽂件」设计思路的⾼明了,不管是设备、另⼀个进程、socket套接字还是真正的⽂件,全部都可以读写,统⼀装进⼀个简单的files数组,进程通过简单的⽂件描述符访问相应资源,具体细节交于操作系统,有效解耦,优美⾼效

线程是什么

⾸先要明确的是,多进程和多线程都是并发,都可以提⾼处理器的利⽤效率,所以现在的关键是,多线程和多进程有啥区别。

为什么说Linux中线程和进程基本没有区别呢,因为从 Linux内核的⾓度来看,并没有把线程和进程区别对待。

我们知道系统调⽤fork()可以新建⼀个⼦进程,函数pthread()可以新建 ⼀个线程。但⽆论线程还是进程,都是⽤task_struct结构表⽰的,唯⼀的区别就是共享的数据区域不同

换句话说,线程看起来跟进程没有区别,只是线程的某些数据区域和其⽗进 程是共享的,⽽⼦进程是拷⻉副本,⽽不是共享。就⽐如说, mm结构和files结构在线程中都是共享的,我画两张图你就明⽩了:

所以说,我们的多线程程序要利⽤锁机制,避免多个线程同时往同⼀区域写⼊数据,否则可能造成数据错乱。 那么你可能问,既然进程和线程差不多,⽽且多进程数据不共享,即不存在数据错乱的问题,为什么多线程的使⽤⽐多进程普遍得多呢?

因为现实中数据共享的并发更普遍呀,⽐如⼗个⼈同时从⼀个账户取⼗元, 我们希望的是这个共享账户的余额正确减少⼀百元,⽽不是希望每⼈获得⼀个账户的拷⻉,每个拷⻉账户减少⼗元。 当然,必须要说明的是,只有Linux系统将线程看做共享数据的进程,不对其做特殊看待,其他的很多操作系统是对线程和进程区别对待的,线程有其特有的数据结构,我个⼈认为不如Linux的这种设计简洁,增加了系统的复杂度。

在Linux中新建线程和进程的效率都是很⾼的,对于新建进程时内存区域拷⻉的问题,Linux采⽤了copy-on-write的策略优化,也就是并不一开始就真正地去复制一份⽗进程的内存空间,⽽是等到需要写内存的操作时才去复制。所以Linux中新建进程和新建线程都是很迅速的。

声明:以上全文摘抄自《labuladong的算法小抄》

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值