《庖丁解牛》第六章书本知识总结
操作系统内个实现操作系统的三大管理功能: 进程管理, 内存管理, 文件系统. 分别对应《操作系统原理》中最重要的 3 个抽象概念是进程, 虚拟内存和文件.
Linux 中的进程描述符 struct task_struct 就是 PCB 进程控制块.
Linux 内核管理的进程状态转换图
进程描述符 struct task_struct 记录了当前进程的父进程 real_parent,parent.
双向链表
struct list_head children
记录当前进程的子进程.
双向链表
struct list_head sibling
记录当前进程的兄弟进程.
fork 系统调用创建了一个子进程, 子进程复制了父进程中所有的进程信息, 包括内核堆栈, 进程描述符等, 子进程作为一个独立的进程也会被调度.
fork,vfork,clone 系统调用和 kernel_thread 内核函数都可以创建一个新进程, 而且都是通过 do_fork 函数来创建进程的, 只不过传递的参数不同.
fork 一个子进程的过程中, 复制父进程的资源时采用了 Copy On Write(写时复制)技术, 不需要修改进程资源, 父子进程是共享内存存储空间的.
do_fork 主要完成了调用 copy_process()复制父进程信息, 获得 pid, 调用 wake_up_new_task 将子进程加入调度器队列等待获得分配 CPU 资源运行, 通过 clone_flags 标志做一些辅助工作.
do_fork 代码:
- long do_fork(unsigned long clone_flags,
- unsigned long stack_start,
- unsigned long stack_size,
- int __user *parent_tidptr,
- int __user *child_tidptr)
- {
- struct task_struct *p;
- int trace = 0;
- long nr;
- // ...
- // 复制进程描述符, 返回创建的 task_struct 的指针
- p = copy_process(clone_flags, stack_start, stack_size,
- child_tidptr, NULL, trace);
- if (!IS_ERR(p)) {
- struct completion vfork;
- struct pid *pid;
- trace_sched_process_fork(current, p);
- // 取出 task 结构体内的 pid
- pid = get_task_pid(p, PIDTYPE_PID);
- nr = pid_vnr(pid);
- if (clone_flags & CLONE_PARENT_SETTID)
- put_user(nr, parent_tidptr);
- // 如果使用的是 vfork, 那么必须采用某种完成机制, 确保父进程后运行
- if (clone_flags & CLONE_VFORK) {
- p->vfork_done = &vfork;
- init_completion(&vfork);
- get_task_struct(p);
- }
- // 将子进程添加到调度器的队列, 使得子进程有机会获得 CPU
- wake_up_new_task(p);
- // ...
- // 如果设置了 CLONE_VFORK 则将父进程插入等待队列, 并挂起父进程直到子进程释放自己的内存空间
- // 保证子进程优先于父进程运行
- if (clone_flags & CLONE_VFORK) {
- if (!wait_for_vfork_done(p, &vfork))
- ptrace_event_pid(PTRACE_EVENT_VFORK_DONE, pid);
- }
- put_pid(pid);
- } else {
- nr = PTR_ERR(p);
- }
- return nr;
- }
copy_process 函数主要完成课调用 dup_task_struct 复制当前进程 (父进程) 描述符 task_struct, 信息检查, 初始化, 把进程状态设置为 TASK_RUNNING(此时子进程置为就绪态), 采用写时复制技术逐一复制所有其他进程资源, 调用 copy_thread 初始化子进程内核栈, 设置子进程 pid 等.
copy_process 代码:
- static struct task_struct *copy_process(unsigned long clone_flags,
- unsigned long stack_start,
- unsigned long stack_size,
- int __user *child_tidptr,
- struct pid *pid,
- int trace)
- {
- int retval;
- struct task_struct *p;
- ...
- retval = security_task_create(clone_flags);// 安全性检查
- ...
- p = dup_task_struct(current); // 复制 PCB, 为子进程创建内核栈, 进程描述符
- ftrace_graph_init_task(p);
- ...
- retval = -EAGAIN;
- // 检查该用户的进程数是否超过限制
- if (atomic_read(&p->real_cred->user->processes)>=
- task_rlimit(p, RLIMIT_NPROC)) {
- // 检查该用户是否具有相关权限, 不一定是 root
- if (p->real_cred->user != INIT_USER &&
- !capable(CAP_SYS_RESOURCE) && !capable(CAP_SYS_ADMIN))
- goto bad_fork_free;
- }
- ...
- // 检查进程数量是否超过 max_threads, 后者取决于内存的大小
- if (nr_threads>= max_threads)
- goto bad_fork_cleanup_count;
- if (!try_module_get(task_thread_info(p)->exec_domain->module))
- goto bad_fork_cleanup_count;
- ...
- spin_lock_init(&p->alloc_lock); // 初始化自旋锁
- init_sigpending(&p->pending); // 初始化挂起信号
- posix_cpu_timers_init(p); // 初始化 CPU 定时器
- ...
- retval = sched_fork(clone_flags, p); // 初始化新进程调度程序数据结构, 把新进程的状态设置为 TASK_RUNNING, 并禁止内核抢占
- ...
- // 复制所有的进程信息
- shm_init_task(p);
- retval = copy_semundo(clone_flags, p);
- ...
- retval = copy_files(clone_flags, p);
- ...
- retval = copy_fs(clone_flags, p);
- ...
- retval = copy_sighand(clone_flags, p);
- ...
- retval = copy_signal(clone_flags, p);
- ...
- retval = copy_mm(clone_flags, p);
- ...
- retval = copy_namespaces(clone_flags, p);
- ...
- retval = copy_io(clone_flags, p);
- ...
- retval = copy_thread(clone_flags, stack_start, stack_size, p);// 初始化子进程内核栈
- ...
- // 若传进来的 pid 指针和全局结构体变量 init_struct_pid 的地址不相同, 就要为子进程分配新的 pid
- if (pid != &init_struct_pid) {
- retval = -ENOMEM;
- pid = alloc_pid(p->nsproxy->pid_ns_for_children);
- if (!pid)
- goto bad_fork_cleanup_io;
- }
- ...
- p->pid = pid_nr(pid); // 根据 pid 结构体中获得进程 pid
- // 若 clone_flags 包含 CLONE_THREAD 标志, 说明子进程和父进程在同一个线程组
- if (clone_flags & CLONE_THREAD) {
- p->exit_signal = -1;
- p->group_leader = current->group_leader; // 线程组的 leader 设为子进程的组 leader
- p->tgid = current->tgid; // 子进程继承父进程的 tgid
- } else {
- if (clone_flags & CLONE_PARENT)
- p->exit_signal = current->group_leader->exit_signal;
- else
- p->exit_signal = (clone_flags & CSIGNAL);
- p->group_leader = p; // 子进程的组 leader 就是它自己
- p->tgid = p->pid; // 组号 tgid 是它自己的 pid
- }
- ...
- if (likely(p->pid)) {
- ptrace_init_task(p, (clone_flags & CLONE_PTRACE) || trace);
- init_task_pid(p, PIDTYPE_PID, pid);
- if (thread_group_leader(p)) {
- ...
- // 将子进程加入它所在组的哈希链表中
- attach_pid(p, PIDTYPE_PGID);
- attach_pid(p, PIDTYPE_SID);
- __this_cpu_inc(process_counts);
- } else {
- ...
- }
- attach_pid(p, PIDTYPE_PID);
- nr_threads++; // 增加系统中的进程数目
- }
- ...
- return p; // 返回被创建的子进程描述符指针 P
- ...
- }
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, 0, NULL, NULL, 0
sys_vfork: CLONE_VFORK | CLONE_VM | SIGCHLD, 0, 0, NULL, NULL, 0
用户栈: 都是父进程的栈.
parent_tidptr, child_ctidptr 都是 NULL.
实验: 分析 Linux 内核创建一个新进程的过程
本次实验中使用的 fork 命令是用 sys_clone 系统调用实现的, 因此断点设置在 sys_clone.
本次实验通过实践, 调试应按照以下顺序进行.
配置好 menuos, 使用 fork 命令
先设置 sys_clone 断点
运行到 sys_clone 后, 设置其它断点 `
进入 do_fork 函数
在 do_fork 函数中会调用 copy_process
在 copy_process 中调用 dup_task_struct
在 copy_process 中调用 copy_thread
子进程 ret
实验过程流程图
参考资料
《庖丁解牛 Linux》 https://item.jd.com/12449500.html
Linux 中 fork,vfork 和 clone 详解(区别与联系)
来源: http://www.bubuko.com/infodetail-2861041.html