1. 进程
进程即处于执行器的程序以及相关资源的总称;线程是在进程中活动的对象(内核调度的对象)。
创建进程:fork()
,创建子进程,从内核返回2次(父进程&子进程),新进程可接着调用exec()
创建新的地址空间(fork()
使用COW)
终结进程:exit()
释放资源,父进程可调用wait4()
查看子进程是否终结(终结的进程处于僵死状态,直到父进程调用wait()
/waitpid()
)
2. 进程描述符和任务结构
进程描述符:struct task_struct
,定义在<linux/shed.h>
2.1. 进程描述符的分配与存放
使用slab分配器分配,达到对象复用和缓存着色。
x86下,获取到task_struct
需要通过thread_info
的成员访问,thread_info
存在内核栈尾端:
1
2
3
4
struct thread_info {
struct task_struct *task;
//...
};
访问thread_info
通过current_thread_info()
函数访问,只要屏蔽栈指针的几个有效位即可,如8KB的栈空间屏蔽后13位。
current
宏可直接获取当前进程的task_struct
,实际上调用current_thread_info()->task
。
current
宏在进程上下文中是有效的,而在中断上下文中是无效的(因为中断程序不会有其它进程干扰)
2.2. 进程状态
TASK_RUNNING
:进程可执行,可能在运行队列准备执行,也可能正在执行(OS课本里的Ready & Running状态)TASK_INTERRUPTABLE
&TASK_UNINTERRUPTABLE
:进程被阻塞,前者可被外部条件中断而投入运行,后者不会(OS课本里Waiting状态)__TASK_TRACED
:正在被其它进程跟踪,如调试时__TASK_STOPPED
:进程停止运行,不能再次投入运行,通常接收到SIGSTOP
,SIGTSTP
,SIGTTIN
,SIGTTOU
时发生(OS课本里Stopped状态)
设置状态:set_task_state(task, state)
/set_current_state(state)
,定义在<linux/sched.h>
2.3. 进程树
最初进程:init
进程,PID为1,所有的其它进程都是它的后代
父进程:可通过task_struct
的parent
访问
子进程:可通过task_struct
的children
访问该链表
兄弟进程:拥有同一个父进程
3. 进程创建
使用fork()
和exec()
。前者通过拷贝当前进程创建一个子进程(区别仅在于PID以及某些资源和统计量),后者将可执行文件载入到地址空间开始运行。
exec()
指exec()
一类函数,有多种实现
3.1. 写时拷贝
Linux的fork()
使用写时拷贝,初始时父进程和子进程共享一份拷贝,只有需要写入时,数据才会被复制。
3.2. fork()
Linux使用clone()
系统调用实现fork()
,然后clone()
回调用do_fork()
创建子进程。通常内核有意选择子进程先执行(若父进程先执行,可能会向地址空间写入)。
父子进程的资源共享可以通过参数调配(如共享打开文件等等)
3.3. vfork()
父进程页表项不会做任何拷贝,其它都相同。
子进程共享父进程的地址空间,且父进程被阻塞,除非子进程退出或执行exec()
。此外,子进程不能像地址空间写入。
4. 线程在Linux下的实现
Linux把所有线程当作进程看待,线程也有task_struct
,只是线程之间共享某些资源(如地址空间)。
如一个进程拥有4个线程,Linux仅创建4个进程并分配4个普通的
task_struct
,然后它们指向某些共享的资源(如地址空间)。
4.1. 创建线程
与进程类似,调用clone()
,但需要指明共享的资源。
4.2. 内核线程
执行内核的后台操作。它独立运行在内核空间,没有独立的地址空间,不切换到用户空间,可被调度和抢占。
5. 进程终结
发生在进程调用exit()
,最后大部分要靠do_exit()
(定义在kernel/exit.c
),释放该进程相关联的所有资源,然后进入EXIT_ZOMBIE
状态。
它依旧占有内核栈,thread_info
和task_struct
,目的是向父进程提供信息,父进程检索或者通知内核,认为这是无关信息后,释放进程的剩余内存,归还给系统。
5.1. 删除进程描述符
父进程得到已终结的子进程信息后,或通知内核这些信息无用后,子进程的task_struct
才会被释放。
wait()
一族函数可知道子进程是否退出,它通过wait4()
实现,动作是:挂起进程,等待其中一个子进程退出,然后返回该子进程的PID
最终释放进程描述符时,调用release_task()
。
5.2. 孤儿进程
父进程提前退出,需要给子进程找一个线程作为父亲(在当前线程组内),若不行,则用init
做父亲(否则子进程会僵死,且驻留着一直占用内存)。
init
会例行调用wait()
检查子进程状态,清除所有僵死进程