侠客 UID:698749 注册:2008-4-29 最后登录: 2008-12-09 帖子: 精华:0 状态: ...离线...
[ ] [ ] [ ]
|
[ ]
发表于 2008-9-28 09:05
|
进程创建: clone, fork, vfork系统调用
-
clone系统调用
-
参数:
-
执行函数(fn), 参数(arg)
-
flags|死亡时给父进程发的信号 (clone_flags): 以下介绍clone_flags
-
资源共享
-
段,页,打开文件共享:
-
页表(不是页, CLONE_VM),
-
打开文件(clone_files),
-
建一个新tls段(clone_settls)
-
路径和权限设置:
-
clone_fs: 共享根目录, 当前目录, 创建文件初始权限.
-
clone_newns: 新的根路径, 自己的视野看文件系统
-
线程通信
-
clone_sighand: 信号处理action, 阻塞和悬挂的信号
-
clone_sysvsem: 共享undoable信号量操作
-
进程关系
-
同父: clone_parent 创建进程与新进程是兄弟 (同父), 新进程不是创建进程的子进程
-
为了方便期间, 以下讨论暂时不考虑这一因素(它很容易实现), 认为创建进程就是父进程
-
同一个线程组: clone_thread. 属于同一个进程(线程组)
-
都被trace: clone_ptrace
-
子进程不被trace: clone_untrace (内核设置, 覆盖clone_ptrace)
-
返回tid
-
向父进程返回tid: clone_parent_settid
-
向子进程返回tid: clone_child_settid
-
子进程的状态:
-
子进程开始就stop: clone_stopped
-
进程死亡或exec通知:
-
启动内核机制: 如果子进程死亡或exec, 它自己空间内的tid(*ctid)清零, 并唤醒等待子进程死亡的进程.
-
赋给子进程的资源
-
子进程的栈(父进程alloc的内存地址)
-
线程局部仓库段(tls)
-
返回子进程tid的地址
-
clone, fork, vfork实现方式
-
大致相同:
-
系统调用服务例程sys_clone, sys_fork, sys_vfork三者最终都是调用do_fork函数完成.
-
do_fork的参数与clone系统调用的参数类似, 不过多了一个regs(内核栈保存的用户模式寄存器). 实际上其他的参数也都是用regs取的
-
区别在于:
-
clone:
-
clone的API外衣, 把fn, arg压入用户栈中, 然后引发系统调用. 返回用户模式后下一条指令就是fn.
-
sysclone: parent_tidptr, child_tidptr都传到了 do_fork的参数中
-
sysclone: 检查是否有新的栈, 如果没有就用父进程的栈 (开始地址就是regs.esp)
-
fork, vfork:
-
服务例程就是直接调用do_fork, 不过参数稍加修改
-
clone_flags:
-
sys_fork: SIGCHLD|0;
-
sys_vfork: SIGCHLD| (clone_vfork | clone_vm)
-
用户栈: 都是父进程的栈.
-
parent_tidptr, child_ctidptr都是NULL.
-
具体实现函数do_fork() (内核函数)的工作流程:
-
分配PID, 确定子进程到底是否traced.
-
分配空闲的PID
-
确定clone_ptrace位. (确定子进程到底要不要被trace, 而不是参数所说的希望被trace)
-
设置该位: 参数已设该位, 且创建线程被trace中
-
清除该位: 父进程没有被trace, 或 clone_untrace已经设置.
-
复制进程描述符(copy_process)
-
检查clone_flags是否兼容, 是否安全
-
clone_newns 与 clone_fs 互斥
-
clone_sighand 是 clone_thread 的必要条件: 线程必须共享信号处理
-
clone_vm 是 clone_sighand 的必要条件 : 共享信号处理, 首先要共享信号处理的代码(在进程页面里)
-
附加的安全检查: security_task_create(clone_flags)
-
复制进程描述符
-
在父进程的thread_info里保存浮点寄存器: __unlazy_fpu()
-
分配新的进程pd(alloc_task_struct), 并拷贝父进程pd
-
分配新的thread_info(alloc_thread_info), 并拷贝父进程的thread_info.
-
新的thread_info和新分配的pd 相互引, 新pd的引用计数设为2 (表示:新pd有用, 且不是僵尸进程)
-
相关计数加1: (此处先相关计数检查, 都通过后再都加1)
-
检查并增加: 用户拥有进程数, 系统总共进程数.
-
一般来说, 所有进程的thread_info总和, 不超过物理内存的1/8
-
新进程的可执行格式的引用计数(FIXME: pd里标有可执行个数吗)
-
系统执行fork总数.
-
进程pd的关键域的设置(顺序与源码可能不一致):
-
进程关系
-
设置父子关系 (parent, real_parent, 考虑被trace的情况)
-
设置新pd的PID
-
设置tgid, 线程组长的pd(pd->group_leader). (根据是不是线程组长, 即clone_thread位是否为0)
-
加入PID哈希表(pid, tgid, 如果是进程组长加入pgid和sid表),(调attach_pid())
-
拷贝tid到父进程的用户空间(parent_tidptr)
-
拷贝资源(如果clone_flags没标明共享):
-
文件,目录,内存:copy_files, copy_mm, copy_namespace,
-
进程通信: copy_signal, copy_sighand, copy_semundo
-
设置子进程的内核栈(thread_info), 内核态相关寄存器(thread_struct, 不知道这个结构的具体用处): copy_thread()
-
子进程的thread_struct:
-
esp, esp0 - 内核栈顶, 内核栈底
-
eip - ret_from_fork()的地址 (用户态切到内核态的第一条指令)
-
I/O许可位图 - 如果父进程有, 就拷贝一份过来
-
TLS - 如果用户空间提供了TLS段, 拷贝过来
-
设置子进程的内核栈:
-
child_regs.esp = 传入的栈地址参数;
-
child_regs.eax = 0, 给用户态的返回值是0
-
清除thread_info中的, TIF_SYSCALL_TRACE位, 防止运行ret_from_fork时, 系统通知调试进程
-
设置子进程的thread_info的cpuid
-
设置调度信息(sched_fork())
-
设置task_running状态,
-
初始化调度参数(时间片),
-
子进程禁止内核抢占(thread_info.preempt_cout = 1)
-
其他:
-
如果没有被trace,pd->ptrace = 0;
-
设置pd->exit_signal:
-
有clone_thread位: 设为参数clone_flags中的退出信号
-
没有clone_thread位: 设为-1 (表示进程终止时, 该LWP不给父进程发信号)
-
pd->flags: 清除PF_SUPERPRIV , 设置PF_FORKNOEXEC
-
大内核锁 pd->lock_depth = -1
-
exec次数: pd->did_exec = 0
-
拷贝child_tidptr到pd->set_child_tid. 以备子进程开始执行时, 把tid放到自己内存空间的child_tidptr
-
返回pd
-
设置父子进程的运行状态, 调度信息
-
设置子进程的状态.
-
挂信号: 如果创建出来的是停止(clone_stopped)或被trace(pd->ptrace里有PT_PTRACE位)的进程, 悬挂一个SIGSTOP信号.
-
只有debugger发出SIGCONT信号后, 才能进入运行状态
-
设状态,入列队:如果有clone_stopped位, 子进程设为stopped状态; 否则调用wake_up_new_task(), 把子进程加入就绪列队:
-
调整父进程和子进程的调度参数 (主要是时间片)
-
如果父子在同一CPU上运行, 且页表不同享, 子进程在插在父进程前
-
子进程很可能exec, 不与父进程共享页. 这样防止父进程无用的copy on write.
-
如果不同CPU上运行, 或者共享页表, 子进程放在列队最后
-
如果父进程处于被调试状态, 程通知调试器
-
当前进程给debugger进程发信号, 告知自己创建了子进程; 并停止自己(进入traced状态), 使debugger运行.
-
子进程的pid保存在current->ptrace_message中, 供debugger用
-
调试器发信号, 使父进程继续后, 再进行下一步; 否则父进程一直处于traced状态
-
设置父进程状态
-
如果有clone_vfork, 把自己放到一个等待列队.
-
内核处理完系统调用后, 会执行调度, 这样就阻塞父进程了.
-
直到子进程释放了它的内存地址空间, 即子进程终止或exec新程序, 用信号唤醒父进程.
-
返回子进程的pid.
-
子进程被调度后,执行pd.thread.eip(ret_from_fork). 调用关系(=>): ret_from_fork=>schedule_tail=>finish_task_switch.
-
schedule_tail的另一件事就是: 把pid保存到地址pd->set_child_tid (创建进程使的parent_tidptr)
-
finish_task_switch的动作是: 装载内核栈保存的寄存器(regs->eax为0),返回到用户态。系统调用返回值就是eax(0)
-
内核线程:
-
只运行于kernel模式,只能访问大于3G的空间。而普通进程在内核模式时,能访问整个4G空间
-
创建方法, 类似于clone
-
准备返回地址fn: 构造一个regs. 里面有fn, args, __KERNEL_CS等. regs->eip是汇编函数kernel_thread_helper
-
do_fork (flags|CLONE_VM|clone_untraced, 0, ®s, 0, NULL, NULL)
-
创建线程, 与父进程共享页. 用上步构造的regs初始化新程的内核栈
-
新线程被调度后. 由ret_from_fork, 用regs恢复寄存器, 开始执行kernel_thread_helper
-
kernel_thread_helper: 把args压入栈, call fn(args, fn都寄存器中)
-
典型的内核线程:
-
进程0: 所有进程的祖先
-
编译时存在.
-
pd, 内核栈: init_task, init_thread_union
-
资源: init_mm, init_files, init_fs. 信号: init_signals, init_sighand
-
页表: swapper_gd_dir
-
功能
-
初始化系统数据,
-
多CPU系统中, 开始时BIOS禁用其他CPU.
-
初始化系统数据后, 进程0拷贝自己到其他CPU的调度列队上, 启动其他CPU, 所有的PID都是0.
-
使能中断
-
创建内核线程1, (函数是init)
-
进入idle
-
进程1:
-
init函数 exec可执行文件init, 使内核线程变成了普通进程.
-
管理其他进程, 称为托孤进程
-
其他内核线程:
-
执行工作列队:
-
ksoftirqd: 执行 softlets
-
kblockd: 执行工作列队 kblockd_workqueue, 定期激活块设备驱动
-
keventd (又叫events): 处理工作列队 keventd_wq
-
管理资源:
-
kapmd: 电源管理
-
kswapd: 交换进程, 用于回收内存资源
-
pdflush: flush脏的磁盘缓存
进程销毁
-
进程终止
-
系统调用
-
整个进程终止: exit_group(), 由do_group_exit处理系统调用. c函数 exit()也是用的这系统调用
-
某个线程终止: _exit(), 由do_exit处理. C函数中用到此系统调用的API: pthread_exit
-
do_group_exit流程: (整个组内至少有一个线程调用它, 用于整组协调)
-
检查线程组的退出过程是否启动: 检查signal_group_exit(线程组内的公共数据)是否非零. 如果没有启动, 执行一下操作来启动退出过程:
-
设置启动标志signal_group_exit.
-
存储终止码(exit_group的参数), 在current->signal->group_exit_cold
-
向其他线程发SIG_KILL信号, (它们收到信号后, 调do_exit())
-
调用do_exit, 使本线程退出
-
do_exit流程:
-
设置线程的终止标志, 退出码
-
设置PF_EXITING, 标明要被终止
-
设置pd->exit_code
-
系统调用参数
-
或是内核提供的错误码, 表示异常终止
-
释放资源:
-
删除该进程的定时器
-
去除对资源的引用:
-
exit_mm, __exit_files;
-
__exit_fs(root路径,工作路径, 创建文件权限), exit_namespace(挂载的文件系统的视野);
-
exit_thread(thread_struct), exit_sem,
-
如果这个线程的函数实现了一种可执行格式, 可执行格式数的引用计数--; FIXME: 还没看到这块儿, 凑合翻译的不一定对
-
改变父子关系, 并向父进程发信号, 改变自己的状态(exit_notify)
-
托付终止线程创建的子进程:
-
如果终止线程还有同组线程: 终止线程创建的子进程, 作为与同组线程的子进程.
-
否则: 终止线程创建的子进程, 作为孤儿进程, 由init进程托管
-
向父进程发信号
-
exit_signal有意义 && 最后线程 : 发exit_signal
-
否则:
-
被trace : 发SIGCHLD
-
没被trace : 不发信号
-
僵尸自己或直接死亡, 并设置PF_DEAD位
-
exit_signal没意义 && 没被trace : 直接死亡 (这种情况没有发信号)
-
变成EXIT_DEAD状态,
-
release_task() (后面介绍). pd引用计数变为1, 不会马上释放
-
否则: 僵尸
-
exit_signal有意义 || 被trace : 僵尸
-
整理"僵尸"与"发临僵尸信号"的关系:
-
将发信号的条件中"最后线程"去掉, 可简化为(exit_signal有意义)||(被trace) == (发信号)
-
可得出后: (发信号) == (僵尸)
-
又可推出: (没有trace && exit_signal有意义 && 不是最后进程) == (僵尸了,但没法信号) , 这种情况在移除死进程时, 会给其父进程发信号 (FIXME: 待验证)
-
调度. 调度函数会忽略僵尸进程, 但会减少僵尸进程的pd的使用计数; 会检查PF_DEAD位, 把它变成exit_dead状态
|
|