进程和线程的区别

前言

​ 一般来说,很多人都是认为进程跟线程的区别是这样的。进程是资源分配的基本单位,线程是CPU调度的基本单位。

​ 虽然,这个说法没什么错。可是实际上,在Linux / Unix这样的操作系统中,进程和线程之间的区别并没有想像中那么大。他们都是用同一种结构体进行描述的,并且创建进程和线程所使用的函数或者说创建进程线程的过程非常类似。

​ 所以说,我们其实可以认为,进程和线程他们是同一种东西。

进程

关于进程的结构体

struct thread_info

​ 在进程的内核栈的底部,有一个数据结构(struct thread_info)。该数据结构里面的字段存储着进程的一些信息。

1633192286077

​ 至于,为什么存在地步。我想可能是为了适配硬件(CPU),因为不知道CPU是否设计有寄存器保留这个thread_info。因此OS的设计者倾向把他放在进程内核栈的底部,这样能很方便在内核中找到对应的进程描述符

1
2
3
4
5
6
7
8
9
10
struct thread_info{
struct task_struct *task; // 进程描述符
struct exec_domain *exe_domain; // 执行信息
unsigned int flag; // 标志
unsigned int status; // 执行状态
unsigned int cpu; // cpu num
int address_limit; //
struct restart_block block; //

}
struct task_struct

​ task_struct 也称为进程描述符,该描述符包含进程的所有信息。

​ 在32位的机器上,进程描述符有1.7K。该描述符包含的信息:进程打开的文件,地址空间,挂起信号,进程状态…

1633193036539

一般来说,内核访问进程(包括进程的调度…means,只要使用到进程),都是通过获得进程的task_struct来进行操作的。

​ 因此,最好就是把task_struct的指针放到寄存器中。但是,可能因为硬件设计的缺陷,并不是所有CPU都有足够多的寄存器去保存task_struct的指针。

因此,把task_struct放到thread_info,在放到进程的内核栈中保存是一种比较高雅的做法了。通过相对位移,就可以计算出thread_info在内核中的位置,然后取得task_struct的指针

关于进程的上下文

​ 简单来说,就是当前进程运行的时候,CPU寄存器所保存的值。如果进行进程调度,需要把CPU的现场保存,这样才能让当前的进程在未来得以恢复。

进程的创建

​ Unix采用了与众不同的进程创建方式。简单来说,分解为两个单独的函数执行fork()和exec()。

​ fork通过拷贝当前进程,创建一个子进程,子进程和父进程的区别仅仅在于PID,PPID,一些资源(根据fork传入的参数确定哪些资源共享),统计量的不同。

​ exec负责读取可执行文件,并且让他载入内存开始运行

内存的写时拷贝

​ 为了加快fork拷贝进程的效率,一开始fork出来的子进程跟父进程是共享内存空间的。

​ 只在需要写入的时候,数据才会真正被复制,往后使得各个进程“真正”意义上拥有自己独立的地址空间。也就是说,资源只有在写入的时候才会被复制,只读的时候共享。这种推迟复制的技术,使得拷贝效率大大提高。

​ 因此,fork的唯一开销,就是复制父进程的描述符。

fork() — > clone系统调用

​ 在Linux里面,fork( )通过在内核中调用clone,实现进程之间的拷贝。

​ 然后,在内核中,clone通过do_fork( )实现大部分进程创建的工作。

  • 位子进程创建内核栈,thread_info,task_struct。所有都与当前进程相同
  • 复制完成后,把task_struct内许多成员设为初始值(主要是统计量)
  • 然后更新一些mode,最主要的有两个内核态权限位 和 执行位
  • 分配pid
  • 然后根据传递的参数决定,拷贝/共享哪些资源
  • 最后返回一个子进程的指针,并且马上exec( )

进程的终结

​ 进程终结其实在Linux里面也具体分为两个步骤。(1)调用exit( ) 系统调用,不管是主动调用,还是被动调用。主动释放进程所拥有的资源。 (2)调用完exit之后,进程实际上已经被销毁,只是进程描述符还没清空,因此需要委托父进程进行销毁。

exit做了哪些工作
  • 删除进程的timer定时器
  • 删除指向内存的指针,如果该内存只有当前进程使用,则释放该内存
  • 把进程开除出IPC调度队列
  • 把进程占用的资源,释放(计数减1)
  • 给子进程重新找养父
  • 这些做完之后,切换到别的进程。
  • 此时进程实际上已经被kill,但是描述符仍旧在 (处于僵死状态)
删除进程描述符

​ 调用exit( )后,尽管线程已经僵死不能再运行,但是系统仍旧保留着进程的描述符。这样做的目的是让,系统能够在子进程终结后,仍能获取它的信息。

​ 因此,系统设计者,把进程析构设计成资源释放和描述符的删除两个步骤。


线程

​ Linux实现线程的机制非常独特。甚至从kernal的角度来说,系统没有线程这个概念。系统没有为线程准备特殊的数据结构和调度算法。

​ 线程仅仅被视为一些共享资源的进程。每个线程都有自己的task_struct。所以在内核中,线程看起来就是进程**(仅仅和其他进程共享地址空间,资源)**

线程的创建

​ 具体操作也是调用fork( )。不过fork传入的参数是,子进程和父进程之间共享的资源。

1
clone(CLONE__VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND , 0)

​ 传递给clone( )的参数,决定了新创建的进程行为方式以及父子之间共享的资源种类。下面是常见的参数。

1633194725511

so,从系统内核的角度来说,进程和线程的区别是不大的,可以说是没有区别