我的愿望是 好好学习 Linux
一, 书本第六章知识总结[进程的描述和进程的创建]
基础知识 1
操作系统内核实现操作系统的三大管理功能, 即进程管理功能, 内存管理和文件系统. 对应的三个抽象的概念是进程, 虚拟内存和文件.
其中, 操作系统最核心的功能是进程管理.
进程标识值: 内核通过唯一的 PID 来标识每个进程.
进程状态: 进程描述符中 state 域描述了进程的当前状态.
iret 与 int 0x80 指令对应, 一个是离开系统调用弹出寄存器值, 一个是进入系统调用压入寄存器的值.
fork()函数最大的特点就是被调用一次, 返回两次, 在父进程中返回新创建子进程的 pid; 在子进程中返回 0.
在 Linux 中, fork,vfork 和 clone 这 3 个系统调用都通过 do_fork 来实现进程的创建.
在 Linux 中 1 号进程是所有用户态进程的祖先, 2 号进程是所有内核线程的祖先.
基础知识 2
在操作系统原理中, 通过进程控制块 PCB 描述进程; 在 Linux 内核中, 通过一个数据结构 struct task_struct 来描述进程.
在操作系统原理中, 进程有就绪态, 运行态和阻塞态; 在 Linux 内核中, 就绪态和运行态都是相同的 TASK_RUNNING 状态另加上一个阻
塞态. 在 Linux 内核中, 当进程是 TASK_RUNNING 状态时, 它是可运行的, 就是就绪态, 是否在运行取决于它有没有获得 CPU 的控制权.
对于一个正在运行的进程, 调用用户态库函数 exit()会陷入内核执行该内核函数 do_exit(), 进程会进入 TASK_ZOMBIE 状态, 即中止状态,
Linux 内核会在适当的时候把该进程处理掉, 后释放进程描述符. 一个正在运行的进程在等待特定事件或资源时会进入阻塞态, 阻塞态分为两
种: TASK_INTERRUPTIBLE 和 TASK_UNINTERRUPTIBLE. 前者可以被信号和 wake_up()唤醒, 后者只能被 wake_up()唤醒. 总结来说分这四种:
TASK_RUNNING: 包括两种状态: 进程就绪且没有运行, 进程正在运行. 这两种状态的区分取决于进程有没有获得 CPU 的分配权.
TASK_ZOMBIE: 进程的终止状态, 此状态的进程被称作僵尸进程, 在此状态下的进程会被 Linux 内核在适当的时候处理掉, 同时进程描述符也将被释放.
TASK_INTERRUPTIBLE : 可以被信号或者是 wake_up()唤醒. 信号来临时, 进程会被设置为 TASK_RUNNING(仅仅是就绪状态而没有执行)
TASK_UNINTERRUPTIBLE: 只能被 wake_up()唤醒
进程状态转换图如下图所示:
进程控制块 PCB--task_struct, 为了管理进程, 内核必须对每个进程进行清晰的描述, 进程描述符提供了内核所需了解的进程信息.
- struct task_struct {
- volatile long state; /* -1 unrunnable, 0 runnable,>0 stopped 进程状态,-1 表示不可执行, 0 表示可执行, 大于 1 表示停止 */
- void *stack; // 内核堆栈
- atomic_t usage;
- unsigned int flags; /* per process flags, defined below 进程标识符 */
- unsigned int ptrace;
如下图:
进程的创建
start_ kernel,rest_init,kernel_ init kthreadd;
start_ kernel 创建了 rest_init, 也就是 0 号进程. 而 0 号进程又创建了两个线程, 一个是 kernel_ init, 也就是 1 号进程, 这个进程最终启动了用户态;
另一个是 kthreadd 内核线程是所有内核线程的祖先, 负责管理所有内核线程.
0 号进程是固定的代码, 1 号进程是通过复制 0 号进程 PCB 之后在此基础上做修改得到的.
创建进程的三个函数
fork, 创建子进程.
vfork, 与 fork 类似, 但是父子进程共享地址空间, 而且子进程先于父进程运行.
clone, 主要用于创建线程.
Linux 通过复制父进程来创建一个新进程, 通过调用 do_ fork 来实现. 然后对子进程做一些特殊的处理. 而 Linux 中的线程, 又是一种特殊的进程. 根
据代码的分析, do_ fork 中, copy_ process 管子进程运行的准备, wake_ up_ new_ task 作为子进程 forking 的完成.
进程创建过程中的四个函数
do_fork(): 创建进程;
copy_process(): 创建进程内容(调用 dup_task_struct, 信息检查, 初始化, 更改进程状态, 复制其他进程资源, 调用 copy_thread 初始化子进
程内核栈, 设置子进程 pid 等);
dup_task_struct(): 复制当前进程 (父进程) 描述符 task_struct, 分配子进程内核栈;
copy_thread(): 内核栈关键信息初始化;
解析 do_fork()
fork,vfork 和 clone 这三个函数最终都是通过 do_fork 函数实现的.
do_fork 的步骤:
调用 copy_process, 将当期进程复制一份出来为子进程, 并且为子进程设置相应地上下文信息.
初始化 vfork 的完成处理信息(如果是 vfork 调用)
调用 wake_up_new_task, 将子进程放入调度器的队列中, 此时的子进程就可以被调度进程选中, 得以运行.
如果是 vfork 调用, 需要阻塞父进程, 知道子进程执行 exec.
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 结构体, 作为子进程的进程描述符.
复制所有的进程信息.
调用 copy_thread, 设置子进程的堆栈信息, 为子进程分配一个 pid.
关于 dup_task_struct
先调用 alloc_task_struct_node 分配一个 task_struct 结构体.
调用 alloc_thread_info_node, 分配了一个 union. 这里分配了一个 thread_info 结构体, 还分配了一个 stack 数组.
返回值为 ti, 实际上就是栈底.
tsk->stack = ti 将栈底的地址赋给 task 的 stack 变量.
最后为子进程分配了内核栈空间.
执行完 dup_task_struct 之后, 子进程和父进程的 task 结构体, 除了 stack 指针之外, 完全相同.
关于 copy_thread 函数
获取子进程寄存器信息的存放位置
对子进程的 thread.sp 赋值, 将来子进程运行, 这就是子进程的 esp 寄存器的值.
如果是创建内核线程, 那么它的运行位置是 ret_from_kernel_thread, - 将这段代码的地址赋给 thread.ip, 之后准备其他寄存器信息, 退出
将父进程的寄存器信息复制给子进程.
将子进程的 eax 寄存器值设置为 0, 所以 fork 调用在子进程中的返回值为 0.
子进程从 ret_from_fork 开始执行, 所以它的地址赋给 thread.ip, 也就是将来的 eip 寄存器.
新的进程从 ret_from_fork 处开始执行
dup_task_struct 中为其分配了新的堆栈.
copy_process 中调用了 sched_fork, 将其置为 TASK_RUNNING.
copy_thread 中将父进程的寄存器上下文复制给子进程, 这是非常关键的一步, 这里保证了父子进程的堆栈信息是一致的.
将 ret_from_fork 的地址设置为 eip 寄存器的值, 这是子进程的第一条指令.
整个详细过程:
二, 实验部分[跟踪分析进程创建的过程]
(1)给操作系统 MenuOS 添加命令(fork 功能)
(2)使用 gdb 调试跟踪
回到 LinuxKernel 目录下, fork 指令实际上执行的就是 sys_clone, 我们可以在 sys_clone,do_fork,dup_task_struct,copy_process,
copy_thread,ret_from_fork 处设置断点, 如下图所示:
*********************************************
*********************************************
*********************************************
*********************************************
最后通过函数 syscall_exit 退出;
三, 实验收获
1. 小总结 1
fork,vfork,clone 都是 Linux 的系统调用, 这三个函数分别调用了 sys_fork,sys_vfork,sys_clone, 最终都调用了 do_fork 函数, 差别在于参数
的传递和一些基本的准备工作不同, 主要用来 Linux 创建新的子进程或线程(vfork 创造出来的是线程).
fork()函数调用成功: 返回两个值; 父进程: 返回子进程的 PID; 子进程: 返回 0; 失败: 返回 - 1;
fork 创造的子进程复制了父亲进程的资源(写时复制技术), 包括内存的内容 task_struct 内容(2 个进程的 pid 不同).
这里是资源的复制不是指针的复制.
vfork 也是创建一个子进程, 但是子进程共享父进程的空间. 在 vfork 创建子进程之后, 父进程阻塞, 直到子进程执行了 exec()或者 exit().
故 vfork 创建出来的不是真正意义上的进程, 而是一个线程, 因为它缺少独立的内存资源.
int clone(int (fn)(void ), void child_stack, int flags, void arg)
clone 和 fork 的区别:
clone 和 fork 的调用方式很不相同, clone 调用需要传入一个函数, 该函数在子进程中执行.
clone 和 fork 最大不同在于 clone 不再复制父进程的栈空间, 而是自己创建一个新的. (void *child_stack,)也就是第二个参数, 需要分配栈指针
的空间大小, 所以它不再是继承或者复制, 而是全新的创造.
2. 小总结 2
进程在创建时具有父子关系, 通过调用 fork()来创建一个新进程. 创建的新进程是从 return_from_fork 开始执行的, 复制内核堆栈只复制了一部分, int
指令和 save_all 压到内核栈的内容. 参数, 系统调用号等都进行压栈. fork,vfork 和 clone 三个系统调用都可以创建一个新进程, 而且都是通过调用 do_fork
来实现进程的创建.
来源: http://www.bubuko.com/infodetail-2861050.html