侠客 UID:698749 注册:2008-4-29 最后登录: 2008-12-09 帖子: 精华:0 状态: ...离线... [] [] [Blog] |
[]
#pid6763306', '已经复制到剪贴板')">2楼 发表于 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状态
| |