《Linux内核分析》第六周学习笔记
《Linux内核分析》第六周学习笔记 进程的描述和创建
郭垚 原创作品转载请注明出处 《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000
【学习视频时间:1小时 撰写博客时间:2小时】
【学习内容:进程创建的过程、使用gdb跟踪分析内核处理函数sys_clone】
一、进程的描述
1.1 进程描述符task_struct数据结构(一)
1. 进程控制块PCB——task_struct
为了管理进程,内核必须对每个进程进行清晰的描述,进程描述符提供了内核所需了解的进程信息。
struct task_struct{ volatile long state; //进程状态,-1表示不可执行,0表示可执行,大于1表示停止 void *stack; //内核堆栈 atomic_t usage; unsigned int flags; //进程标识符 unsigned int ptrace; …… }
struct task_ struct数据结构很庞大
- Linux进程的状态与操作系统原理中的描述的进程状态似乎有所不同,比如就绪状态和运行状态都是TASK_ RUNNING,为什么呢?
- 进程的标示pid
- 所有进程链表struct list_ head tasks;
- 内核的双向循环链表的实现方法
- 一个更简略的双向循环链表
- 程序创建的进程具有父子关系,在编程时往往需要引用这样的父子关系。进程描述符中有几个域用来表示这样的关系
- Linux为每个进程分配一个8KB大小的内存区域,用于存放该进程两个不同的数据结构:Thread_ info和进程的内核堆栈
- 进程处于内核态时使用,不同于用户态堆栈,即PCB中指定了内核栈,那为什么PCB中没有用户态堆栈?用户态堆栈是怎么设定的?
- 内核控制路径所用的堆栈很少,因此对栈和Thread_ info来说,8KB足够了
- struct thread_ struct thread; //CPU-specific state of this task
- 文件系统和文件描述符
- 内存管理——进程的地址空间
Linux进程状态转换图
1.2 进程描述符task_struct数据结构(二)
1242#ifdef CONFIG_SMP //条件编译,多处理器会用到 1243 struct llist_node wake_entry; 1244 int on_cpu; 1245 struct task_struct *last_wakee; 1246 unsigned long wakee_flips; 1247 unsigned long wakee_flip_decay_ts; 1248 1249 int wake_cpu; 1250#endif 1251 int on_rq; 1252 1253 int prio, static_prio, normal_prio; 1254 unsigned int rt_priority; //与优先级相关 1255 const struct sched_class *sched_class; 1256 struct sched_entity se; 1257 struct sched_rt_entity rt; …… 1295 struct list_head tasks; //进程链表 1296#ifdef CONFIG_SMP 1297 struct plist_node pushable_tasks; 1298 struct rb_node pushable_dl_tasks; 1299#endif
Linux的状态图
1301 struct mm_struct *mm, *active_mm; //内存管理进程的地址空间 1302#ifdef CONFIG_COMPAT_BRK 1303 unsigned brk_randomized:1; 1304#endif 1305 /* per-thread vma caching */ 1306 u32 vmacache_seqnum; 1307 struct vm_area_struct *vmacache[VMACACHE_SIZE]; 1308#if defined(SPLIT_RSS_COUNTING) 1309 struct task_rss_stat rss_stat; 1310#endif 1311/* task state */ 1312 int exit_state; //任务的状态 1313 int exit_code, exit_signal; 1314 int pdeath_signal; /* The signal sent when the parent dies */ 1315 unsigned int jobctl; /* JOBCTL_*, siglock protected */ 1316 1317 /* Used for emulating ABI behavior of previous Linux versions */ 1318 unsigned int personality; 1319 1320 unsigned in_execve:1; /* Tell the LSMs that the process is doing an 1321 * execve */ 1322 unsigned in_iowait:1; …… 1337 //进程的父子关系 1338 * pointers to (original) parent process, youngest child, younger sibling, 1339 * older sibling, respectively. (p->father can be replaced with 1340 * p->real_parent->pid) 1341 */ 1342 struct task_struct __rcu *real_parent; /* real parent process */ 1343 struct task_struct __rcu *parent; /* recipient of SIGCHLD, wait4() reports */ 1344 /* 1345 * children/sibling forms the list of my natural children 1346 */ 1347 struct list_head children; /* list of my children */ 1348 struct list_head sibling; /* linkage in my parent's children list */ 1349 struct task_struct *group_leader; /* threadgroup leader */
1356 struct list_head ptraced; //调试 1357 struct list_head ptrace_entry; 1358 1359 /* PID/PID hash table linkage. */ 1360 struct pid_link pids[PIDTYPE_MAX]; 1361 struct list_head thread_group; 1362 struct list_head thread_node; //将进程链表连接起来 …… 1411/* 与当前任务CPU状态相关,对进程上下文切换有关键性作用 */ 1412 struct thread_struct thread; 1413/* filesystem information */ 1414 struct fs_struct *fs; 1415/* 打开文件描述符列表 */ 1416 struct files_struct *files; 1417/* namespaces */ 1418 struct nsproxy *nsproxy; 1419/* 信号处理 */ 1420 struct signal_struct *signal; 1421 struct sighand_struct *sighand; 1422 1423 sigset_t blocked, real_blocked; 1424 sigset_t saved_sigmask; /* restored if set_restore_sigmask() was used */ 1425 struct sigpending pending; …… 1598 struct pipe_inode_info *splice_pipe; 1599 //与管道相关 1600 struct page_frag task_frag; 1601 1602#ifdef CONFIG_TASK_DELAY_ACCT 1603 struct task_delay_info *delays; 1604#endif 1605#ifdef CONFIG_FAULT_INJECTION 1606 int make_it_fail; 1607#endif
二、进程的创建
2.1 进程的创建概览及fork一个进程的用户态代码
回顾:
- 道生一(start_ kernel...cpu_ idle)
- 一生二(kernel_ init和kthreadd)
- 二生三(即前面的0、1、2三个进程)
- 三生万物(1号进程是所有用户态进程的祖先,2号进程是所有内核线程的祖先)
- start_ kernel创建了cpu_ idle,也就是0号进程。而0号进程又创建了两个线程,一个是kernel_ init,也就是1号进程,这个进程最终启动了用户态;另一个是kthreadd。0号进程是固定的代码,1号进程是通过复制0号进程PCB之后在此基础上做修改得到的
- iret与int 0x80指令对应,一个是弹出寄存器值,一个是压入寄存器的值
- 如果将系统调用类比于fork();那么就相当于系统调用创建了一个子进程,然后子进程返回之后将在内核态运行,而返回到父进程后仍然在用户态运行
fork一个子进程的代码
#include <stdio.h> #include <stdlib.h> #include <unistd.h> int main(int argc, char * argv[]) { int pid; /* fork another process */ pid = fork(); if (pid < 0) { /* error occurred */ fprintf(stderr,"Fork Failed!"); exit(-1); } else if (pid == 0) //pid == 0和下面的else都会被执行到(一个是在父进程中即pid ==0的情况,一个是在子进程中,即pid不等于0) { /* child process */ printf("This is Child Process!\n"); } else { /* parent process */ printf("This is Parent Process!\n"); /* parent will wait for the child to complete*/ wait(NULL); printf("Child Complete!\n"); } }
2.2 理解进程创建过程复杂代码的方法
Linux中创建进程一共有三个函数:
- fork,创建子进程
- vfork,与fork类似,但是父子进程共享地址空间,而且子进程先于父进程运行。
- clone,主要用于创建线程
这里值得注意的是,Linux中得线程是通过模拟进程实现的,较新的内核使用的线程库一般都是NPTL。
-
复制一个PCB——task_struct
err = arch_dup_task_struct(tsk, orig);
-
给新进程分配一个新的内核堆栈
ti = alloc_ thread_ info_ node(tsk, node); tsk->stack = ti; setup_ thread_ stack(tsk, orig); //这里只是复制thread_ info,而非复制内核堆栈
- 要修改复制过来的进程数据,比如pid、进程链表等。具体见copy _process内部
- 从用户态的代码看fork(),函数返回了两次,即在父子进程中各返回一次。
2.3 浏览进程创建过程相关的关键代码
通过之前的学习,我们知道fork是通过触发0x80中断,陷入内核,来使用内核提供的提供调用:
SYSCALL_DEFINE0(fork) { return do_fork(SIGCHLD, 0, 0, NULL, NULL); } #endif SYSCALL_DEFINE0(vfork) { return do_fork(CLONE_VFORK | CLONE_VM | SIGCHLD, 0, 0, NULL, NULL); } SYSCALL_DEFINE5(clone, unsigned long, clone_flags, unsigned long, newsp, int __user *, parent_tidptr, int __user *, child_tidptr, int, tls_val) { return do_fork(clone_flags, newsp, 0, parent_tidptr, child_tidptr); }
通过以上精简后的代码,我们可以看出,fork、vfork和clone这三个函数最终都是通过do_ fork函数实现的。
追踪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; }
由以上代码可以看出,do_ fork大概做了如下几件事:
- 调用copy_ process,将当期进程复制一份出来为子进程,并且为子进程设置相应地上下文信息。
- 初始化vfork的完成处理信息(如果是vfork调用)
- 调用wake_ up_ new_ task,将子进程放入调度器的队列中,此时的子进程就可以被调度进程选中,得以运行。
- 如果是vfork调用,需要阻塞父进程,知道子进程执行exec。上面的过程对vfork稍微做了处理,因为vfork必须保证子进程优先运行,执行exec,替换自己的地址空间。
抛开vfork,进程创建的大部分过程都在copy_ process函数中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; // 分配一个新的task_struct,此时的p与当前进程的task,仅仅是stack地址不同 p = dup_task_struct(current); // 检查该用户的进程数是否超过限制 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; } retval = -EAGAIN; // 检查进程数量是否超过 max_threads,后者取决于内存的大小 if (nr_threads >= max_threads) goto bad_fork_cleanup_count; // 初始化自旋锁 // 初始化挂起信号 // 初始化定时器 // 完成对新进程调度程序数据结构的初始化,并把新进程的状态设置为TASK_RUNNING retval = sched_fork(clone_flags, p); // ..... // 复制所有的进程信息 // copy_xyz // 初始化子进程的内核栈 retval = copy_thread(clone_flags, stack_start, stack_size, p); if (retval) goto bad_fork_cleanup_io; if (pid != &init_struct_pid) { retval = -ENOMEM; // 这里为子进程分配了新的pid号 pid = alloc_pid(p->nsproxy->pid_ns_for_children); if (!pid) goto bad_fork_cleanup_io; } /* ok, now we should be set up.. */ // 设置子进程的pid p->pid = pid_nr(pid); // 如果是创建线程 if (clone_flags & CLONE_THREAD) { p->exit_signal = -1; // 线程组的leader设置为当前线程的leader p->group_leader = current->group_leader; // tgid是当前线程组的id,也就是main进程的pid p->tgid = current->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; // tgid和pid相同 p->tgid = p->pid; } if (clone_flags & (CLONE_PARENT|CLONE_THREAD)) { // 如果是创建线程,那么同一线程组内的所有线程、进程共享parent p->real_parent = current->real_parent; p->parent_exec_id = current->parent_exec_id; } else { // 如果是创建进程,当前进程就是子进程的parent p->real_parent = current; p->parent_exec_id = current->self_exec_id; } // 将pid加入PIDTYPE_PID这个散列表 attach_pid(p, PIDTYPE_PID); // 递增 nr_threads的值 nr_threads++; // 返回被创建的task结构体指针 return p; }
根据这份精简代码,我总结出copy_process的大体流程:
- 检查各种标志位(已经省略)
- 调用dup_ task_ struct复制一份task_ struct结构体,作为子进程的进程描述符。
- 检查进程的数量限制。
- 初始化定时器、信号和自旋锁。
- 初始化与调度有关的数据结构,调用了sched_ fork,这里将子进程的state设置为TASK_ RUNNING。
- 复制所有的进程信息,包括fs、信号处理函数、信号、内存空间(包括写时复制)等。
- 调用copy_ thread,这又是关键的一步,这里设置了子进程的堆栈信息。
- 为子进程分配一个pid
- 设置子进程与其他进程的关系,以及pid、tgid等。这里主要是对线程做一些区分。
2.4 创建的新进程从哪里开始执行?
一个新创建的子进程,(当它获得CPU之后)是从哪一行代码进程执行的?
- 与之前写过的my_ kernel相比较,kernel中是可以指定新进程开始的位置(也就是通过eip寄存器指定代码行)。fork中也有相似的机制
-
这涉及子进程的内核堆栈数据状态和task_ struct中thread记录的sp和ip的一致性问题,这是在copy_ thread in copy_ process设定的
*childregs = *current_pt_regs(); //复制内核堆栈 childregs->ax = 0; //子进程的fork返回0 p->thread.sp = (unsigned long) childregs; //调度到子进程时的内核栈顶 p->thread.ip = (unsigned long) ret_from_fork; //调度到子进程时的第一条指令地址
2.5 使用gdb跟踪创建新进程的过程(实验)
1. 更新menu,删除test_fork.c和test.c文件,重新执行make rootfs
2. 编译内核查看fork命令
3. 启动gdb调试,并对主要的函数设置断点
执行一个fork,会发现只输出一个fork的命令描述,后面并没有执行,因为它停在了sys_ clone这个位置。
4. 特别关注新进程是从哪里开始执行的?为什么从哪里能顺利执行下去?即执行起点与内核堆栈如何保证一致。
答:ret_ from_ fork决定了新进程的第一条指令地址。子进程从ret_ from_ fork处开始执行。因为在ret_ from_ fork之前,也就是在copy_ thread()函数中* childregs = * current_ pt_ regs();该句将父进程的regs参数赋值到子进程的内核堆栈。* childregs的类型为pt_ regs,里面存放了SAVE_ ALL中压入栈的参数,因此在之后的RESTORE ALL中能顺利执行下去。
总结
Linux中所有的进程创建都是基于复制的方式,Linux通过复制父进程来创建一个新进程,通过调用do_ fork来实现。然后对子进程做一些特殊的处理。而Linux中的线程,又是一种特殊的进程。根据代码的分析,do_ fork中,copy_ process管子进程运行的准备,wake_ up_ new_ task作为子进程forking的完成。fork()函数最大的特点就是被调用一次,返回两次。