说到进程,即便笔试中最常见的问题就是线程和进程的关系了,这么先说一下答案:在Linux系统中,进程和线程几乎没有区别。
Linux中的进程也许就是一个数据结构,顺带可以理解文件描述符、重定向、管道命令的底层工作原理,最后我们从操作系统的角度瞧瞧为何说线程和进程基本没有区别。
一、进程是哪些
首先,具象地来说,我们的计算机就是这个东西:
这个大的圆形表示计算机的显存空间,其中的小圆形代表进程,左下角的方形表示c盘,右下角的图形表示一些输入输出设备,例如键盘按键显示器等等。另外,注意到显存空间被界定为了两块,上半部份表示用户空间,下半部份表示内核空间。
用户空间装着用户进程须要使用的资源,例如你在程序代码里开一个字段,这个字段肯定存在用户空间;内核空间储存内核进程须要加载的系统资源,这一些资源通常是不容许用户访问的。并且注意有的用户进程会共享一些内核空间的资源,例如一些动态链接库等等。
我们用C语言写一个hello程序,编译后得到一个可执行文件,在命令行运行就可以复印出一句helloworld,之后程序退出。在操作系统层面,就是新建了一个进程,这个进程将我们编译下来的可执行文件读入显存空间,之后执行,最退后出。
你编译好的那种可执行程序只是一个文件,不是进程,可执行文件必需要载入显存,包装成一个进程能够真正跑上去。进程是要借助操作系统创建的,每位进程都有它的固有属性,例如进程号(PID)、进程状态、打开的文件等等linux解压命令,进程创建好以后,读入你的程序,你的程序才被系统执行。
这么linux 命令,操作系统是怎样创建进程的呢?对于操作系统,进程就是一个数据结构,我们直接来看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中一切都被具象成文件,设备也是文件,可以进行读和写。
假如我们写的程序须要其他资源,例如打开一个文件进行读写,这也很简单,进行系统调用linux查看进程的线程数,让内核把文件打开,这个文件才会被放在files的第4个位置,对应文件描述符3:
明白了这个原理,输入重定向就挺好理解了,程序想读取数据的时侯才会去files[0]读取,所以我们只要把files[0]指向一个文件,这么程序都会从这个文件中读取数据,而不是从按键:
同理,输出重定向就是把files[1]指向一个文件,这么程序的输出就不会写入到显示器,而是写入到这个文件中:
错误重定向也是一样的,就不再赘言。
管线符也许也是异曲同工,把一个进程的输出流和另一个进程的输入流接起一条「管道」,数据就在其中传递,不得不说这些设计思想真的很巧妙:
到这儿,你可能也看出「Linux中一切皆文件」设计思路的高明了,不管是设备、另一个进程、socket套接字还是真正的文件,全部都可以读写,统一装进一个简单的files字段,进程通过简单的文件描述符访问相应资源,具体细节交于操作系统,有效前馈linux查看进程的线程数,优美高效。
三、线程是哪些
首先要明晰的是,多进程和多线程都是并发,都可以提升处理器的借助效率,所以现今的关键是,多线程和多进程有啥区别。
为何说Linux中线程和进程基本没有区别呢,由于从Linux内核的角度来看,并没有把线程和进程区别对待。
我们晓得系统调用fork()可以新建一个子进程,函数pthread()可以新建一个线程。但无论线程还是进程,都是用task_struct结构表示的,惟一的区别就是共享的数据区域不同。
换句话说,线程看上去跟进程没有区别,只是线程的个别数据区域和其父进程是共享的,而子进程是拷贝副本,而不是共享。就例如说,mm结构和files结构在线程中都是共享的,我画两张图你就明白了:
所以说,我们的多线程程序要借助锁机制,防止多个线程同时往同一区域写入数据,否则可能导致数据错乱。
这么你可能问,既然进程和线程差不多,但是多进程数据不共享,即不存在数据错乱的问题,为何多线程的使用比多进程普遍得多呢?
由于现实中数据共享的并发更普遍呀,例如十个人同时从一个帐户取十元,我们希望的是这个共享帐户的余额正确降低一百元,而不是希望每人获得一个帐户的拷贝,每位拷贝帐户降低十元。
其实,必需要说明的是,只有Linux系统将线程看做共享数据的进程,不对其做特殊看待,其他的好多操作系统是对线程和进程区别对待的,线程有其特有的数据结构,我个人觉得不如Linux的这些设计简约,降低了系统的复杂度。
在Linux中新建线程和进程的效率都是很高的,对于新建进程时显存区域拷贝的问题,Linux采用了copy-on-write的策略优化,也就是并不真正复制父进程的显存空间,而是等到须要写操作时才去复制。所以Linux中新建进程和新建线程都是很迅速的。
以上就是全部内容,假如有帮助的话,不妨点个在看,我瞧瞧操作系统相关的文章阅读数据如何样,不错的话之后可以再写写操作系统方面的小知识。