进程

进程(process)就是处于执行期的程序(动态的)。Linux内核中通常也叫任务(task)。内核通过一个唯一的进程标识值(PID)来标识每个进程。

进程描述符

进程描述符是task_struct类型结构,它的字段包含了与一个进程相关的所有信息。(内核还定义了task_t数据类型等同于task_struct

该结构在<linux/sched.h>中定义:

process_descriptor

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_structflags成员。表明进程是否拥有超级用户权限的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_structexit_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内核(第三版)》