进程,轻量级进程和Posix线程
OS教科书对进程和线程的区别有这样简单的概括:进程是资源分配的单位,线程是调度的单位。但是Linux内核是不区分进程和线程的,都用任务(task_struct)来表示,什么时候叫进程什么时候叫线程,需要视上下文而定。虽没有专门的线程实现,但内核用轻量级进程LWP(Light Weight Process)来代替线程。
内核好比服务器,用户任务好比客户端,内核的一切都是为用户态服务的。轻量级进程就是为了支持用户态线程库。
你的系统上用的是什么线程库?
Posix thread是符合posix标准线程库的统称,具体实现有多种,在此不赘述,只要知道线程库的实现按照用户态线程数和内核进程数的比例可分为三种模型:N:1, M:N和1:1。执行下面的命令可以查看你的系统上用的是什么线程库:
NPTL(Native Posix Thread Lib)是Linux的线程实现,用的是1:1模型。这里有篇文章将的很好,请参考NPTL分析之线程的创建。
轻量级进程
轻量级进程就是linux所谓的“线程”,它只是共享了父进程很多资源如进程地址空间,文件描述符和信号量等,而独立的资源只有任务描述符task_struct,内核栈等与调度有关的,做到了:资源共享,调度独立。
共享进程地址空间struct task_struct {struct mm_struct *mm;}
是通过让多个进程的mm
指向同一个虚拟内存struct mm_struct
实现的。通过pthread_create()创建线程时,会通过clone()系统调用传入各种flags组合来决定哪些资源需要与父进程共享。
struct mm_struct
表示了一个进程虚拟地址空间,虚拟地址空间的layout很多参考书都有,struct mm_struct {unsigned long start_code, end_code, start_data, end_data; unsigned long start_brk, brk, start_stack; unsigned long arg_start, arg_end, env_start, env_end;}
,代码,数据,堆,栈,参数,环境变量各段空间都有指向。而这些虚拟内存段与物理内存的映射关系是怎么建立的呢?答案是通过内存映射mmap。VMA(Virtual Memory Area)列表struct mm_struct {struct vm_area_struct *mmap; /* list of VMAs */}
, 有些vma如动态库,可执行程序是通过mmap, 有些vma如堆,栈是通过匿名映射完成的。
进程组或线程组
进程描述符里有个字段struct task_struct { struct list_head thread_group;}
,明显是想把一个线程组的线程放在一起。进程组struct task_struct { pid_t tgid }
, tgid就是线程组里领头进程struct task_struct {struct task_struct *group_leader;}
的pid。
其他
LinuxThreads是用户空间的线程库,所采用的是线程-进程1对1模型(即一个用户线程对应一个轻量级进程,而一个轻量级进程对应一个特定 的内核线程),将线程的调度等同于进程的调度,调度交由内核完成,而线程的创建、同步、销毁由核外线程库完成(LinuxThtreads已绑定到 GLIBC中发行)。
在LinuxThreads中,由专门的一个管理线程处理所有的线程管理工作。当进程第一次调用pthread_create()创建线程时就会先 创建(clone())并启动管理线程。后续进程pthread_create()创建线程时,都是管理线程作为pthread_create()的调用 者的子线程,通过调用clone()来创建用户线程,并记录轻量级进程号和线程id的映射关系,因此,用户线程其实是管理线程的子线程。
1、进程、轻量级进程、线程、线程组之间的关系
2、及它们的标识相关说明
一、进程、轻量级进程、线程、线程组之间的关系
借助上图说明:
进程P0有四条执行流,即线程,
主线程t0是它的第一个线程,且与进程P0相关联,
之后衍生出t1、t2、t3三个线程,这三个线程与轻量级进程P1、P2、P3一一关联,
所有的进程、轻量级进程、线程组成了线程组。
轻量级进程也是进程,只不过它与某进程的某特定线程相关联。
二、它们的标识相关说明
pid是进程标识符,tgid是线程组标识符
每个进程都有自己的pid,如图中:进程pid(P0)= a,轻量级进程pid(P1)= b / pid(P1)= c / pid(P1)= d。
同属于一个线程组的所有进程、轻量级进程有同样的线程组标识符,且其为第一个线程所关联的进程标识符,
例如:图中第一个线程为t0,它所关联的进程为P0,pid(P0)= a,所以tgid(P1)= a / tgid(P1)= a / tgid(P1)= a / tgid(P1)= a。
当我们使用函数getpid(current_p)时,返回值不是current_p的pid,而是它的tgid。(current_p为当前进程)
一个程序至少有一个进程,一个进程至少有一个线程. 线程的划分尺度小于进程,使得多线程程序的并发性高。
另外,进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率。
同一个进程的多个子线程在进程的共享内存中分配独立的栈空间
栈:是个线程独有的,保存其运行状态和局部自动变量的。栈在线程开始的时候初始化,每个线程的栈互相独立,因此,栈是thread safe的。每个C++对象的数据成员也存在在栈中,每个函数都有自己的栈,栈被用来在函数之间传递参数。操作系统在切换线程的时候会自动的切换栈,就是切换SS/ESP寄存器。栈空间不需要在高级语言里面显式的分配和释放。
在Linux系统上每一个线程,实际上都有独立的2个栈空间,用户栈空间和内核栈空间。(注意是每个线程)
进程和线程在Linux上的唯一区别就是同一个进程的不同线程共享进程的地址空间,仅此而已。
tgid:线程组id (多个线程构成)
pid: 进程id (进程等同于一个线程组?)
ppid: 父进程id
pgid: 进程组id (由多个进程构成) 其作用是简化向组内所有的进程发送信号
sid: 会话组id (由多个进程组构成) 在一个bash上运行的程序都归属于一个session(除非特别处理),而这个bash就是这个session的leader。每个session又可以关联一个控制终端
父进程退出不会影响子进程, 但是领头会话组的进程退出,整个会话组内的进程都会退出, 这也是要使用nohup的原因