1.进程与线程
linux
中调度与执行代码流的基础单位是线程.
我们通过父进程产生新的子进程,其实是产生一个新的线程.不过这个线程属于一个新的线程组,且是线程组的组长.
我们通过兄弟线程p
产生新的线程q
.也是获得新线程的方法.不过这个新线程q
和兄弟线程p
在同一个线程组.
每个线程拥有一个task_struct
对象实例来记录信息和管理自身.每个线程拥有一个独立的线程id
.
每个线程必然属于一个线程组,每个线程组拥有一个组长.
2.线程的状态
(1). TASK_RUNNING
要么线程正在cpu
中执行,要么线程位于cpu
运行队列,可供调度执行.
(2). TASK_INTERRUPTIBLE
线程由于需要阻塞等待某些条件的满足而位于相应的等待队列,无法被调度执行.但若系统或其他线程给线程发了信号,且希望线程立即处理信号时,线程可被唤醒以便执行信号处理.唤醒并执行信号处理后,线程会从阻塞处继续运行,应用代码应负责进一步判断是否应该继续.
(3). TASK_UNINTERRUPTIBLE
线程由于需要阻塞等待某些条件的满足而位于相应的等待队列,无法被调度执行.系统或其他线程给线程发了信号时,线程也无法被唤醒.
(4). TASK_TRACE
调试追踪用途.
(5). TASK_STOPPED
调试追踪用途.
(6). EXIT_ZOMBIE
一般表示线程已经退出,但其task_struct依然未被回收的阶段.
3.线程id
每个线程拥有独立的id
,称为线程id
.
通过父进程产生子进程时,linux
内核实际产生一个新线程,但这个新线程属于一个新的线程组,且自己是线程组的组长.其线程id
就是这个线程组组长的id
.
linux
内核采用哈希表方式来通过id
快速定位到task_struct
结构.
为了实现通过线程id
,快速定位到线程的task_struct
结构,linux
内核准备了一个全局哈希表p1
.每个线程需将其<id,task_struct对象指针>
注册到此哈希表.
为了实现通过线程组组长id
,快速定位到线程组组长的task_struct
结构,linux
内核准备了一个全局哈希表p2
.每个线程组组长需将其<id,task_struct对象指针>
注册到此哈希表.
为了快速获得一个线程组下的所有成员,p2
哈希表的链式结构的节点额外包含一个链表结构.通过此链表结构可以将此节点代表的线程组的每个成员链接起来.
当基于父进程产生子进程时,产生的新线程.除了形成一个新的线程组外.还将隶属于父进程所在的进程组,隶属于父进程所在的会话,隶属于父进程所在的终端对象.这部分主要服务于信号处理,不太常用,不做深入分析.
4.线程关系
(1). 父子关系
基于父进程创建子进程时,产生的新线程的task_struct
结构中real_parent
指向执行fork
调用的task_struct
实例(父进程).
基于兄弟线程创建新线程时,产生的线程的task_struct
结构中real_parent
和执行fork
调用的task_struct
中real_parent
一致(和兄弟的父相同).
新线程产生时,其task_struct
中real_parent
和parent
是一致的.
(2). 兄弟关系
基于兄弟线程创建新线程时,新线程和创建其的线程是兄弟关系.
对代表某个线程的task_struct
而言,其children
字段指向一个双向链表,链表中包含线程的所有孩子线程.
对代表某个线程的task_struct
而言,其sibling
字段是一个双向链表的节点,该双向链表中的所有其他成员均是线程的兄弟线程.
5.基于兄弟线程产生新线程
新线程共享兄弟线程的信号处理,用户态虚拟内存空间,打开文件结构.共享的意思是共享的资源仅需递增引用数即可.
6.基于父进程产生子进程
父子进程的用户态虚拟地址空间是独立的,独立的意思是父子进程拥有各自独立的页表结构,拥有各自独立的用户态线性区管理结构.新进程页表的内容设置参考父进程的页表来.
但针对父进程页表中属于私有映射的页表项,在处理时,会分别取消父子进程中对应页表项的可写权限下.这样,后续父子进程需对相应线性地址执行写访问时,将在缺页异常处理中通过写时拷贝来解决异常.
7.线程退出
线程退出时,会重新为其孩子线程选择父亲.
在线程所在线程组尚有存活的线程时,选择其中一个作为其孩子线程的父亲.
在线程所在线程组无存活的线程时,选择init
全局线程作为其孩子线程的父亲,init
会对已经释放的线程执行回收调用,来释放其task_struct
实例.
一般而言linux
中非detach
线程退出时,其task_struct
结构依然存在,直到执行回收调用时,才释放.
而detach
线程,一般退出时,直接释放其task_struct
,无需执行回收调用.