进程
进程(process)就是处于执行期的程序(动态的)。Linux内核中通常也叫任务(task)。内核通过一个唯一的进程标识值(PID)来标识每个进程。
进程描述符
进程描述符是task_struct
类型结构,它的字段包含了与一个进程相关的所有信息。(内核还定义了task_t
数据类型等同于task_struct
)
该结构在<linux/sched.h>
中定义:
thread_info
存放进程的基本信息,每个任务的的thread_info
结构在它内核栈的尾端(栈底,向下增长的栈;栈顶,向上增长的栈)。
struct thread_info {
struct task_struct *task;
struct exec_domain *exec_domain;
__u32 flags;
__u32 status;
__u32 cpu;
int preempt_count;
mm_segment_t addr_limit;
struct restart_block restart_block;
void *sysenter_return;
int uaccess_err;
};
进程状态
进程描述符中的state
来表示进程的当前状态。
进程必定处于下列状态中的一种:
TASK_RUNNING(可运行状态)
进程是可执行的:它或者正在执行,或者在运行队列中等待执行。
TASK_INTERRUPTIBLE(可中断的等待状态)
进程被阻塞或挂起或睡眠,等待某个条件达成。
TASK_UNINTERRUPTIBLE(不可中断等待状态)
除了就算是接受到信号也不会被唤醒或等待运行外,这个状态与可中断的等待状态相同
TASK_TRACED(跟踪状态)
被其他进程跟踪的进程
TASK_STOP(停止状态)
进程停止执行;进程没有投入运行也不能投入运行。
进程间的关系
Linux系统中的每个进程必有一个父进程。相应的,每个进程也可以拥有零个或多个子进程与孩子进程。拥有同一个父进程的所有进程被称为兄弟进程。进程间的关系存放在进程描述符中。每个task_struct
都包含一个指向其父进程task_strcut
,叫做parent
的指针,还包含一个称为children
的子进程链表。
struct task_struct *task;
/* 获取链表中的下个进程 */
list_entry(task->tasks.next, struct task_struct, tasks);
/* 获取链表中的前个进程 */
list_entry(task->tasks.prev, struct task_struct, tasks);
for_each_process(task) {
/* 遍历链表中的每个进程,并打印它们的PID */
printk("%s[%d]\n", task->comm, task->pid);
}
进程创建
创建进程的机制:首先在新的地址空间里创建进程,读入可执行文件,最后开始执行。Linux中使用fork()
与exec()
实现进程机制。
Linux的fork()
使用写时拷贝(copy-on-write)页实现。写时拷贝是让父进程和子进程共享同一个拷贝,只有在需要写入的时候,数据才会被复制,从而使各个进程拥有各自的拷贝。也就是说资源的复制只有在需要写入的时候才进行,在次之前,只是以只读方式共享。
Linux使用do_fork
完成了创建中的大部分工作,定义在kernel/fork.c
文件中。该函数调用copy_process()
函数,然后让进程开始运行。copy_process()
函数完成工作过程:
1) 调用dup_task_struct()
为新进程创建一个内核栈,thread_info
结构和task_struct
,这些值与当前进程的值相同。此时,子进程和父进程的描述符是完全相同。
2) 检查并确保新创建这个子进程后,当前用户所拥有的进程数目没有超出给它分配的资源限制。
3) 子进程着手使自己与父进程区别开来。进程描述符内的许多成员都要被清0或设为初始值。那些不是继承而来的进程描述符成员,主要是统计信息。
4) 子进程的状态被设置为TASK_UNINTERRUPTIBLE
,以保证它不会投入运行。
5) copy_process()
调用copy_flags()
以更新task_struct
的flags
成员。表明进程是否拥有超级用户权限的PF_SUPERPRIV
标志被清0。表明进程还没有调用exec()
函数的PF_FORKNOEXEC
标志被设置。
6) 调用alloc_pid()
为新进程分配一个有效的PID
。
7) 根据传递给clone()
的参数标志,copy_process()
拷贝或共享打开的文件、文件系统信息、信号处理函数、进程地址空间和命名空间等。在一般情况下,这些资源会被给定进程的所有线程共享;否则,这些资源对每个进程是不同的,因此被拷贝到这里。
8) 最后,copy_process()
做扫尾工作并返回一个指向子进程的指针。
Linux中的线程
在Linux中将线程定义为特殊的进程。内核并没有准备特别的调度算法或定义特别的数据结构来表示线程。线程仅仅被视为一个与其他进程共享某些资源的进程。
创建线程
线程的创建与普通进程的创建类似,只不过在调用clone()
的时候需要传递一些参数表示来指明需要共享的资源:
clone(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND, 0);
内核线程
内核线程和普通进程间的区别在于内核线程没有独立的地址空间(实际上指向地址空间的mm指针被设置为NULL)。它们只在内核空间运行。
/* 创建并不运行内核线程 */
struct task_struct *kthread_create(int (*threadfn)(void *data), void *data, const char namefmt[], ... );
/* 创建并运行内核线程 */
struct task_struct *kthread_run(int (*threadfn)(void *data), void *data, const char namefmt[], ... );
内核线程启动后就一直运行到调用do_exit()
退出,或者内核的其他部分调用ktread_stop()
退出,传递给ktread_stop()
的参数为ktread_create()
函数返回的task_struct结构的地址:
int ktread_stop(struct task_struct *k);
进程终结
当一个进程终结时,内核必须释放它所占有的资源并把这一不幸的消息告知其父进程。
进程终极的大部分工作依靠do_exit()
来完成,定义与kernel/exit.c
,其过程如下:
1) 将task_struct
中的标志成员设置为PF——EXITING
。
2) 调用del_timer_sync()
删除任一内核定时器。根据返回结果,确保没有定时器排队,也没有定时器处理程序,在运行。
3) 如果BSD的进程记账功能是开启的,do_exit()
调用acct_update_integrals()
来输出记账信息。
4) 然后调用exit_mm()
函数释放进程占用的mm_struct
,如果没有别的进程使用他们(也就是说,这个地址空间没有被共享),就彻底释放它们。
5) 接下来调用sem_exit()
函数。如果进程排队等候IPC信号,它则离开队列。
6) 调用exit_files()
和exit_fs()
,以分别递减文件描述符、文件系统数据的引用计数。如果其中某个引用计数器的数值降为零,那么就代表没有进程在使用相应的资源,此时可以释放。
7) 接着把存放在task_struct
的exit_code
成员中的任务退出代码置为由exit()
提供的退出代码,或者去完成任何其他由内核机制规定的退出动作。退出代码存放在这里供父进程随时索引。
8) 调用exit_notify()
向父进程发送信号,给子进程重现找新的父进程,新的父进程为线程组中的其他线程或是init进程,并把进程状态(存放在task_struct
结构的exit_state
中)设成EXIT_ZOMBIE
.
9) do_exit()
调用schedule()
切换到新的进程。
此时的进程为僵尸进程,存在的唯一目的就是向它的父进程提供信息。父进程检索到信息后,或者通知内核那是无关的信息后,由进程所持有的剩余内存被释放,归还给系统使用。
删除进程描述符
当最终需要释放进程描述符时,release_task()
会被调用,以完成一下工作:
1) 它调用__exit_signal()
,该函数调用_unhash_process()
,后者又调用detach_pid()
从pidhash上删除该进程,同时也要从任务列表中删除该进程。
2) _exit_signal()
释放目前僵尸进程所使用的所有资源,并进行最终统计和记录。
3) 如果这个进程是线程组最后一个进程,并且领头进程已经死掉,那么release_task()
就要通知僵尸进程的领头进程的父进程。
4) release_task()
调用put_task_struct()
释放进程内核栈和thread_info结构所占的页,并释放task_struct
所占的slab高速缓存。
参考
《Linux内核设计与实现(第三版)》
《深入理解Linux内核(第三版)》