进程管理相关的系统调用
- fork: 负责进程的复制,系统调用完成后,生成当前进程一个副本
- exec: 负责加载新的应用程序
进程的复制
-
传统Unix系统中用于复制进程的系统调用是fork, 但在Linux上它并不是唯一的系统调用
- fork:使用copy-on-write机制建立了一个进程的副本,然后作为子进程运行。
- vfork: 用于子进程形成后立即执行exceve加载新程序的情形,因为fork使用了COW机制,vfork在速度方面已经没有优势
- clone: 产生线程,可以对父子进程之间的共享,复制进行精确控制。
-
Copy on write
-
COW技术可以防止复制父进程的所有数据,节省了创建进程的时间,主要利用了下述的事实:代码段无需复制而且进程通常只使用了其内存页的一小部分,尤其是对于复制后即将执行execve系统调用的程序而言,复制没有任何意义。
-
内核通过复制进程的页表的方式来达到进程地址空间复制的目的,同时将页表标记为只读,未来如果发生写操作,内核可以检查改地址空间是否可写来判断是否需要映射新的物理内存。
-
-
执行系统调用
- fork, vfork和clone的系统调用入口点分别为: sys_fork, sys_vfork和sys_clone,定义依赖于具体的体系结构。
- 上述函数完成参数提取相关的工作,统一调用do_fork函数进行进程复制
-
do_fork实现
-
函数原型以及说明
long do_fork(unsigned long clone_flags, unsigned long stack_start, struct pt_regs *regs, unsigned long stack_size, int __user *parent_tidptr, int __user *child_tidptr)
-
clone_flags: 标志集合,控制复制过程的一些属性,最低字节指定了子进程中止时发送给父进程的信号
-
stack_start: 用户空间栈的起始地址
-
regs: 寄存器的集合
-
stack_size: 用户空间栈的大小
-
parent_tidptr, child_tidptr: 指向父子进程的TID, NPTL库的线程实现需要这两个参数
-
不同的fork变体,主要通过clone_flags来区分,fork系统调用
asmlinkage int sys_fork(struct pt_regs regs) { return do_fork(SIGCHLD, regs.esp, ®s, 0, NULL, NULL); }
sys_vfork:
asmlinkage int sys_vfork(struct pt_regs regs) { return do_fork(CLONE_VFORK | CLONE_VM | SIGCHLD, regs.esp, ®s, 0, NULL, NULL); }
sys_clone(用户空间来指定标志集合):
asmlinkage int sys_clone(struct pt_regs regs) { unsigned long clone_flags; unsigned long newsp; int __user *parent_tidptr, *child_tidptr; clone_flags = regs.ebx; newsp = regs.ecx; parent_tidptr = (int __user *)regs.edx; child_tidptr = (int __user *)regs.edi; if (!newsp) newsp = regs.esp; return do_fork(clone_flags, newsp, ®s, 0, parent_tidptr, child_tidptr); }
-
-
do_fork流程
- copy_process
- Determine PID
- Initialize vfork completion handler (only with CLONE_VFORK) and ptrace flags
- wake_up_new_task
- wait_for_completion (only with CLONE_VFORK)
-
说明
- fork系统调用要返回新进程的PID,如果设置了CLONE_NEWPID标志,fork操作可能创建了新的PID命名空间,此时要返回发出fork系统调用的进程所在命名空间的进程ID
nr = (clone_flags & CLONE_NEWPID) ? task_pid_nr_ns(p, current->nsproxy->pid_ns) : task_pid_vnr(p);
- 如果使用Ptrace监控新进程,进程创建后立即发送SIGSTOP信号,以便调试器检查其数据
- wake_up_new_task唤醒新进程,即将其task_struct加入调度队列
- 对于vfork,借助于wait_for_completion函数,父进程在vfork_done上休眠,直到子进程退出或者执行exec系统调用
- fork系统调用要返回新进程的PID,如果设置了CLONE_NEWPID标志,fork操作可能创建了新的PID命名空间,此时要返回发出fork系统调用的进程所在命名空间的进程ID
-
-
进程复制
-
流程
- Check flags
- dup_task_struct
- Check resource limits
- Initialize task structure
- sched_fork
- Copy/share process components
- copy_semundo
- copy_files
- copy_fs
- copy_sighand
- copy_signal
- copy_mm
- copy_namespaces
- copy_thread - Set IDs, task relationships, etc.
-
Check flags, 主要是检查某些标志组合,比如:
- 创建新命名空间和共享所有文件系统信息不能同时设定, CLONE_NEWNS和CLONE_FS
- CLONE_THREAD创建一个线程时,必须使用CLONE_SIGHAND激活信号共享
-
dup_task_struct, 建立父进程的task_struct副本,父子进程的task_struct实例只有一个成员不同,新进程分配了一个新的核心态栈: task_struct->stack,通常和thread_info一同保存在一个联合中,thread_info保存了特定于体系结构部分相关的数据。
union thread_union { struct thread_info thread_info; unsigned long stack[THREAD_SIZE/sizeof(long)]; }; struct thread_info { struct task_struct *task; /* main task structure */ struct exec_domain *exec_domain; /* execution domain */ unsigned long flags; /* low level flags */ unsigned long status; /* thread-synchronous flags */ __u32 cpu; /* current CPU */ __s32 preempt_count; /* 0 => preemptable,< 0 => BUG*/ mm_segment_t addr_limit; /* thread address space */ struct restart_block restart_block; };
flags: 保存进程的各种标志,TIF_SIGPENDING置位表示进程有待决信号,TIF_NEED_RESCHED表示进程应该或者想要调度器选择另外一个进程
cpu: 进程正在其上执行的CPU
preempt_count: 实现内核抢占需要的计数器
resart_block: 用于实现信号机制 -
检查资源限制
-
当前用户的资源计数器,保存在user_struct实例中,可以通过task_struct->user访问
if (atomic_read(&p->user->processes) >= p->signal->rlim[RLIMIT_NPROC].rlim_cur) { if (!capable(CAP_SYS_ADMIN) && !capable(CAP_SYS_RESOURCE) && p->user != current->nsproxy->user_ns->root_user) goto bad_fork_free; }
-
sched_fork
- 调度器对新进程进行设置,初始化一些统计字段,在SMP系统上,有可能需要在各个CPU之间对可用的进程重新均衡一下,此外将进程的状态设置为TASK_RUNNING,防止内核其他部分在进程设置彻底完成前调度进程
-
进程资源的复制(copy_xyz)
- task_struct包含了一些指针,指向可共享和可复制的内核子系统的资源,参考下图:
对于资源res_abc和res_def,最初父子进程task_struct的指针都指向了资源的同一个实例,如果CLONE_ABC置位,则两个进程会共享res_abc,此外,还需要对实例的引用计数加1,如果CLONE_ABC没有置位,会为子进程创建abs_res的一份副本,其资源计数器初始化为1 - COPY相关标志概述
COPY_SYSVSEM: 置位则copy_semundo使用父进程的System V信号量
CLONE_FILES:置位则copy_files使用父进程的文件描述符,否则创建新的files结构
CLONE_FS:置位则copy_fs使用父进程的文件系统上下文,包括根目录,进程的当前工作目录等
CLONE_SIGHAND,CLONE_THREAD:置位则copy_sighand使用父进程的信号处理程序
CLONE_THREAD:置位则copy_sighand与父进程使用信号处理程序中不特定于处理程序的部分
COPY_MM:置位则copy_mm让父进程和子进程共享同一个地址空间,如果没有置位,则使用COW复制进程的一份页表
copy_namespaces:用于建立子进程的命名空间,和前述语义相反,如果没有指定CLONE_NEWxyz,则与父进程共享相应的命名空间
copy_thread:与其他的复制操作不同,这个是一个特定于体系结构的函数
- task_struct包含了一些指针,指向可共享和可复制的内核子系统的资源,参考下图:
-
进程ID以及进程关系设置
-
填充task_struct对父子进程不同的各个成员,例如sibling, children, 间隔定时器成员cpu_timers以及信号待决列表pending
-
pid = alloc_pid(task_active_pid_ns§);
struct pid *alloc_pid(struct pid_namespace *ns) { struct pid *pid = kmem_cache_alloc(ns->pid_cachep, GFP_KERNEL); tmp = ns; for (i = ns->level; i >= 0; i--) { nr = alloc_pidmap(tmp); if (nr < 0) goto out_free; pid->numbers[i].nr = nr; pid->numbers[i].ns = tmp; tmp = tmp->parent; } pid->level = ns->level; for (i = ns->level; i >= 0; i--) { upid = &pid->numbers[i]; hlist_add_head_rcu(&upid->pid_chain, &pid_hash[pid_hashfn(upid->nr, upid->ns)]); } }
-
进程ID设置
p->pid = pid_nr(pid); p->tgid = p->pid; // 对于线程,线程组ID与调用clone/fork的进程相同 if (clone_flags & CLONE_THREAD) p->tgid = current->tgid; if (clone_flags & (CLONE_PARENT|CLONE_THREAD)) p->real_parent = current->real_parent; // 线程,父进程是分支进程的父进程 else p->real_parent = current; //普通进程,父进程是分支进程 p->parent = p->real_parent; p->group_leader = p; // 对于线程来说,其组长是当前进程的组长 if (clone_flags & CLONE_THREAD) { p->group_leader = current->group_leader; list_add_tail_rcu(&p->thread_group, &p->group_leader->thread_group); } if (likely(p->pid)) { add_parent(p); if (thread_group_leader(p)) { if (clone_flags & CLONE_NEWPID) p->nsproxy->pid_ns->child_reaper = p; set_task_pgrp(p, task_pgrp_nr(current)); set_task_session(p, task_session_nr(current)); attach_pid(p, PIDTYPE_PGID, task_pgrp(current)); attach_pid(p, PIDTYPE_SID, task_session(current)); list_add_tail_rcu(&p->tasks, &init_task.tasks); __get_cpu_var(process_counts)++; } attach_pid(p, PIDTYPE_PID, pid); ... return p;
-
-
-
线程创建的问题
- 用户空间线程库使用clone系统调用来生成新线程,其有一些特殊的标志对copy_process有影响,NPTL实现多线程的特殊标志有:
- CLONE_PARENT_SETTID将生成线程的PID复制到执行clone系统调用的某个用户空间地址上
- CLONE_CHILD_SETTID传递一个用户空间地址给内核,保存在新生成task_struct的set_child_tid中,新进程第一次执行时,将当前pid复制到该用户空间地址上
- CLONE_CHILD_CLEARTID也传递一个用户空间地址给内核,在进程结束时,将0写入该地址
- CLONE_CHILD_SETTID和CLONE_CHILD_CLEARTID可以用来检测内核中线程的产生和销毁
- 用户空间线程库使用clone系统调用来生成新线程,其有一些特殊的标志对copy_process有影响,NPTL实现多线程的特殊标志有:
内核线程
- 内核直接启动的线程,主要任务:
- 周期性将修改的内存页和页来源块设备同步
- 如果内存页很少使用,将其写入到交换区
- 管理延时动作
- 实现文件系统的事务
- 相关函数
int kernel_thread(int (*fn)(void *), void * arg, unsigned long flags) struct task_struct *kthread_create(int (*threadfn)(void *data), void *data, const char namefmt[], ...)
- 惰性TLB处理,由于内核线程不与任何的用户层程序相关,内核不需要切换虚拟地址空间的用户层部分,如果内核线程运行之后的进程和之前是同一个,内核并不需要修改用户空间地址表,TLB中的信息仍然有效
启动新程序
-
execve函数主要流程
- 打开可执行文件
- bprm_init
- prepare_binprm
- mm_alloc
- init_new_context
- __bprm_mm_init
- 复制环境和参数数组内容
- search_binary_handler
-
主要过程说明
- mm_alloc生成一个新的mm_struct来管理进程的地址空间
- init_new_context是特定于系统结构的函数
- __bprm_mm_init建立初始的堆栈,新进程的各个参数(egid, euid, 参数列表,环境,文件名等)随后会分别传递给其他函数,因此合并成一个类型为linux_binprm的结构
- search_binary_handler查找一种适当的二进制格式,用于所要执行的特定文件
- 解释二进制格式
load_binary:加载普通程序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 *); int (*core_dump)(long signr, struct pt_regs * regs, struct file * file); unsigned long min_coredump; /* minimal dump size */ };
load_shlib:加载动态库
core_dump :程序出错时输出内存转储
退出进程
- 进程通过调用exit终止,对应调用入口点sys_exit,需要一个错误码作为其参数,该调用使得内核有机会将该进程使用的资源释放回系统。