The Process
1.process
进程不仅仅只是一个程序段,还包括打开的文件,收到的信号,进程状态,地址空间,一个或多个运行线程,保存全局变量的数据段等等
2.thread
线程拥有独立的PC,栈以及一组寄存器。linux内核是调度线程而不是进程,因为linux不区分线程和进程,线程只是一种特殊的进程。
3.fork(),exec(),wait(),waitpid()
进程调用fork()通过复制调用进程来创建的新的进程,fork()返回两次,一次在调用进程,一次在子进程中。
exec()函数创建一个新的地址空间,将新的程序加载进去。
fork()一般是通过clone()来实现的。
程序通过exit()系统调用退出,这个函数终止进程,并释放所有该进程的资源。
父进程可以通过wait4()系统调用来获得已经终止的子进程状态。当一个进程退出(exit)的时候,它会变成一个特殊zombie状态来代表已经终止的进程直到它的父进程调用wait()或者waitpid()函数。
Porcess Descriptor and the Task structure
1.Allocating the Process Descriptor
进程描述符task_structure是通过slab分配器来分配的,这个数据结构一般被保存在每个进程内核栈的最后,而现在是通过slab分配的来分配一种新的结构: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;
};
每个进程的thread_info都被保存在它的内核栈的最后,其中有一个指向task_structure结构的指针。
2.Storing the Process Descriptor
系统通过PID来识别不同的进程,PID的类型为pid_t,实际上是int。
系统可以很快的查找当前进程的PID通过一个current宏,在不同的体系结构中这个宏是不一样的。
x86架构有效利用了thread_info被保存在内核栈中这个事实来计算thread_info的地址。主要的两条汇编指令:
movl $-8192, %eax
andl %esp, %eax
将栈指针%esp的最后13位置0,这也表示内核栈大小为8KB。
使用current_thread_info()->task可以获得当前进程的task_struct。
3.Process State
进程描述符中的state记录该进程的状态,分别有以下5种:
1. TASK_RUNNING 处于该状态的进程要么正在运行要么在队列中等待被调度。
2. TASK_INTERRUPTIBLE 进程被阻塞,等待某个外部条件满足,当外部条件满足或者收到某个信号时被唤醒。
3. TASK_UNINTERRUPTIBLE 与状态2唯一不同的是当它收到某个信号的时候不会被唤醒。
4. __TASK_TRACED 表示进程正在被追踪。
5. __TASK_STOPPED 进程运行停止,一般是收到以下信号后:SIGSTOP,SIGTSTP,SIGTTIN,SIGTTOU。
通过 set_task_state(task, state)来改变进程的状态。

4.Process Context
普通程序运行的用户态,当一个程序执行一个系统调用或者引发一个异常的时候,会进入内核态。这时内核就处在进程上下文中,此时current宏有效。离开内核时,如果存在优先级更高的进程处在TASK_RUNNING状态,那么调度器会调度那个进程运行而不是原进程。
5.The Process Family Tree
在linux进程中,所有进程都是init进程的后代,init的PID是1。
在进程描述符中有指向父进程的parent指针和一组指向儿子进程的指针children。
内核通过以下两种方法来遍历子进程和寻找父进程
struct task_struct *task;
struct list_head *list;
list_for_each(list, ¤t->children){
task = list_entry(list, struct task_struct,sibling);
}
for (task = current; task != &init_task; task = task->parent);
;
由于进程是由双向循环列表组织的,因此通过下面方式可以遍历所有进程:
list_entry(task->tasks.next, struct task_struct, tasks);
/* or */
list_entry(task->tasks.prev, struct taks_struct, tasks)
Process Creation
通过fork()调用产生的子进程和父进程有以下不同:1.PID和PPID 2.接受到的信号
1.Copy-on-Write
为了提高效率,linux使用写时复制的方式来fork()进程。
fork()时唯一的开销是复制进程页表和创建进程描述符。复制资源只发生在他们其中一个需要写的时候,在此之前都是只读并且共享资源。
2.forking
fork()调用的详细过程:
1. 陷入系统调用clone(),clone()间接调用 do_fork(),do_fork()完成了创建进程的大部分工作。
2. do_fork()调用copy_process()函数,它完成以下工作:
(1)调用dup_task_struct(),为新进程创建新的内核栈以及thread_info,task_struct结构,所有这些新创建的内容都和父进程中是相同的。
(2)检查以保证新进程创建后不会超过进程数的上限。
(3)进程状态被改变为TASK_UNINTERRUPTIBLE。
(4)调用copy_flags()修改进程的描述符的flags成员。
(5)调用allloc_pid()申请新的PID。
(6)根据传递给clone()的参数flags,复制或共享打开的文件,文件系统信息,信号处理函数,进程地址空间等等。
(7) copy_process()返回指向新进程的指针。
3.如果copy_process()返回成功,新进程被唤醒并且运行。
3.vfork()
vfork()会产生一个新的子进程.但是vfork创建的子进程与父进程共享数据段,而且由vfork创建的子进程将先于父进程运行.fork()的使用详见百度词条fork().
vfork()用法与fork()相似.但是也有区别,具体区别归结为以下3点:
vfork()用法与fork()相似.但是也有区别,具体区别归结为以下3点:
1. fork():子进程拷贝父进程的数据段,代码段. vfork():子进程与父进程共享数据段.
2. fork():父子进程的执行次序不确定.
vfork():保证子进程先运行,在调用exec或_exit之前与父进程数据是共享的,在它调用exec
或_exit之后父进程才可能被调度运行。
3. vfork()保证子进程先运行,在她调用exec或_exit之后父进程才可能被调度运行。如果在
调用这两个函数之前子进程依赖于父进程的进一步动作,则会导致死锁。
4.当需要改变共享数据段中变量的值,则拷贝父进程。
看上去好像节省了开销,但是呢,由于写时复制的引用,vfork()唯一的优势就是没有复制父进程的页表项。
因此,调用vfork()之后子进程应该紧跟着执行evec(),且不能依赖于任何父进程的行为。
The linux Implementation of Threads
上面我们说到linux是没有thread这个概念的,那么为什么呢?
因为在linux中thread就是一个process,但是这个process是和其他的process共享资源的,每个thread都有独立的进程描述符,仅仅和其他process共享资源,像地址空间等等。
1.Creating Threads
我们讨论以下clone()中的参数flags。
线程和进程的创建方式类似,唯一的不同是传给clone()的参数flags
通常是这样:
/* thread */
clone(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND, 0);
/* normal process */
clone(SIGCHLD,0);
/* vfork() */
clone(CLONE_VFORK | CLONE_VM | SIGCHLD, 0);
2.Kernel Threads
内核线程和不同进程的不同是不存在地址空间,他们仅仅在内核态运行永远不会切换到用户态
可以使用 ps -ef 来查看当前系统中的内核线程。内核线程在系统引导时被其他内核线程的创建
内核的创建的运行通过以下宏定义实现,内核线程会一直存在直到调用do_exit()或者被内核的其他部分调用thread_stop()。
#define kthread_run(threadfn, data, namefmt, ...) \ ({ \
struct task_struct *k; \
k = kthread_create(threadfn, data, namefmt, ## __VA_ARGS__); \
if (!IS_ERR(k)) \
wake_up_process(k); \
k; \
})
int kthread_stop(struct task_struct *k)
Process Termination
进程可以显式调用exit()退出,也可以通过接受一个它不能处理和忽略的信号来退出。无论怎么终止,大部分的工作都是通过do_exit()来处理的。
下面的do_exit()的详细过程:
1.将taks_struct中的flags设为PF_EXITING。
2.调用 del_timer_sync() 来删除内核计时器,一旦删除就保证没有计时器在队列中且没有计时器处理函数正在运行。
3.调用exit_mm()来释放mm_struct的资源,如果没有其他进程使用该地址空间,内核就销毁它。
4.调用exit_sem(),如果进程正在某个IPC信号的等待队列中,让它出队。
5.调用exit_files()和exit_fs(),减少相关文件或文件系统的引用计数。
6.关于exit_code,抱歉我实在没有看懂这一段= =。。。。
7.调用exit_notify()来发送信号给父进程,并给子进程重新寻找父亲,设置当前进程的exit_state为EXIT_ZOMBIE。
8.调用do_exit()并使调度器切换至另一个进程。do_exit()不返回!
这样的话就只剩下进程的内核栈,thread_info和task_struct所占用的资源没有释放。
1.Removing the Process Descriptor
wait()函数通过调用wait4()来实现,它挂起当前进程直到有子进程的信号出现,并返回那个子进程的PID。
有了PID我们就可以释放process descriptor了,内核是通过调用release_task()来实现的,具体步骤如下:
1.release_task() -> _exit_signal() -> __unhash_process() -> detach_pid() 从pidhash和进程队列中删除进程。
2._exit_signal()释放所有其他的占用资源。
3.如果该进程是线程组中的最后一个成员,并且该组的leader是一个僵尸进程,那么release_task()通知leader的父进程。
4.release_task()调用put_task_struct()释放包含该进程内核栈和thread_info的页,释放slab中相关的缓存。
2.The Dilemma of Parentless Task
考虑父进程比子进程先退出的情况,如果不加处理的话,一旦这些子进程变成zombie状态,就没有父进程能够帮助销毁Process Descriptor和相关资源了,因此需要reparent!