《第二章》进程管理和调度 - 续
进程管理之进程复制
三个系统调用--- fork, vfork, clone。
vfork不创建父进程数据的副本,fork使用了写时复制(copy-on-write)技术后,vfork已无用武之地。
clone产生线程,可以对父子进程之间的共享、复制进程进行精确的控制。
写时复制(copy-on-write COW)--- 并不复制整个地址空间,只复制页表,即父子进程使用相同的物理内存页。但父子进程都不能对页面写(因为是父子进程共享物理页),当父进程或者子进程需要写入时,CPU向内核报告访问错误(缺页异常),内核检查额外的数据结构,发现这是因为COW造成的,于是创建一个新的物理页给写进程,并修改这一条映射条目。
fork之后
子进程写
fork、vfork、clone最终调用do_fork。
long do_fork(unsignedlong clone_flags,
unsignedlong stack_start,
struct pt_reg *regs,
unsignedlong stack_size,
int __user *parent_tidptr,//for NativePosixThreads Library
int __user *children_tidptr); // as the previos parameters
不同的fork(fork,vfork,clone使用不同的Flags)
fork --- Flag = SIGCHLD
vfork --- Flag = SIGCHLD | CLONE_VFORK | CLONE_VM
clone --- Flag = regs.ebx (非硬编码)
vfork机制下,子进程会启动完成机制(completions mechanism),父进程调用wait_for_completion函数睡眠,直到子进程终止或者调用execve。这可以保证vfork的父进程会一直处于不活动的状态直到子进程退出或者执行一个新程序。父进程临时的睡眠状态,也确保了两个进程不会彼此干扰或操作对方的地址。
(TIPS:Linux内核经常在函数调用成功时返回一个指针,出错时返回错误码,因此将错误码编码成指针。Linux支持的所有体系结构的0~4KB的空间都是没有意义的,所以指向这部分空间的指针不会是一个正确的指针,因此这部分空间对应的指针值被用来作为错误码的编码)
父子进程的task_struct->stack不同。stack通常与thread_info一同保存在同一个联合中。
union thread_union {
struct thread_info thread_info;
unsignedlongstack[THREAD_SIZE/sizeof(long)];
};
thread_info存放了特定于体系结构的汇编代码所需要访问的关于进程的数据。
struct thread_info {
struct task_struct *task; /* main task structure */
struct exec_domain *exec_domain; /* execution domain */
unsignedlongflags; /* low level flags */
unsignedlongstatus; /* thread-synchronous flags */
__u32 cpu; /* current CPU */
intpreempt_count; /* 0 => preemptable, <0 => BUG */
mm_segment_t addr_limit; /* thread address space */
struct restart_block restart_block;
}
内核线程
内核线程是由内核本身启动的线程,主要用于执行如下任务:周期性同步内存页和设备、swap、管理延时动作(?)、实现文件系统的事务日志。
内核线程主要有两类:启动后等待内核请求内核线程执行某一操作;启动后周期性检查并操作。
调用kernel_thread可以启动一个内核线程(其实现与体系结构相关):
int kernel_thread(int(*fn)(void*), void* args, unsignedlong flags);
内核线程的task_struct的mm指针指向NULL,因为内核线程不能访问用户空间,但是内核希望知道用户空间当前包含了什么,所以在active_mm中保存了指向mm的指针。注:mm指针为空的进程被称为惰性TLB(lazy TLB)进程,因为内核并不需要修改用户地址表,TLB中的信息仍然有效。
其他创建方式:
struct task_struct *kthread_create(int (*threadfn)(void *data),void *data,constchar namefmt[],...);
宏 kthread_run 调用kthread_create.
启动新程序
execve系统调用
int do_execve( char * filename, char __user *__user *argv, char __user *__user *envp, struct pt_regs * regs );
TIPS: __user注释允许自动化工具来检测是否所有的相关事宜处理妥当。
mm_alloc分配一个新的mm_struct,init_new_context用来初始化该实例,__bprm_mm_init则建立初始的栈。prepare_binprm用于提供一些父进程相关的值。search_binary_handler用于在do_execve结束时查找一种适当的二进制格式,用于需要执行的特定文件。二进制格式处理程序负责将新程序的数据加载到旧的地址空间。主要的操作:
1 释放原进程的资源
2 将应用程序映射到虚拟地址空间:text、data、heap、stack、arg、env
3 设置PC和其他寄存器,以便调度到该进程时执行main函数
struct linux_binfmt {
struct linux_binfmt * next;
struct module *module;
int (*load_binary)(struct linux_binprm *, struct pt_regs * regs);
int (*load_shlib)(struct file *);/* load share lib */
int (*core_dump)(long signr, struct pt_regs * regs, struct file * file);
unsignedlong min_coredump; /* minimal dump size */
};
退出进程
exit系统调用。主要工作:将各个引用计数器减1,若为0,回收。