分析进程创建的过程---linux内核学习笔记(六)
内容一:实验报告相关说明
所学课程:《Linux内核分析》MOOC课程
链接:http://mooc.study.163.com/course/USTC-1000029000
内容二:进程控制块简析
为了管理进程,内核必须对每个进程进行清晰的描述,内核所需了解的进程信息都记录在结构体 task_struct 中,所以进程控制块PCB
又被成为进程描述符。
2.1 部分变量解释
1237 void *stack; //进程堆栈 1239 unsigned int flags; /* per process flags, defined below */ 1242 #ifdef CONFIG_SMP //代表多核情况 1251 int on_rq; //运行队列 1253 int prio, static_prio, normal_prio; //优先级 1255 const struct sched_class *sched_class;//进程调度相关
1413 /* 文件系统信息 */ 1414 struct fs_struct *fs; 1415 /*打开的文件描述符列表 */ 1416 struct files_struct *files;
1419 /*信号处理相关*/ 1420 struct signal_struct *signal; 1421 struct sighand_struct *sighand;
2.2 进程链表
1295 struct list_head tasks; //结构声明如下 23 struct list_head { 24 struct list_head *next, *prev; 25 };
作用,构建双向链表:
2.3
//进程内存相关 1301 struct mm_struct *mm, *active_mm; //记录进程的PID,以及PID的哈希表 1330 pid_t pid; 1331 pid_t tgid;
1360 struct pid_link pids[PIDTYPE_MAX];
2.4 进程的父子关系描述部分
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 */
其关系描述可以简化为如下:
2.5 进程与cpu相关状态
1412 struct thread_struct thread; //结构实现 468 struct thread_struct { 470 struct desc_struct tls_array[GDT_ENTRY_TLS_ENTRIES]; 471 unsigned long sp0; 472 unsigned long sp; ...... 483 unsigned long ip; ...... 525 };
类似与之前所学的mypcb中记录的IP和SP
内容三:进程的创建
linux系统允许任何一个用户创建一个子进程,创建之后,子进程存于系统之中,并且独立于父进程。该子进程可以接受调度,可以分配得到系统资源。系统中,除了0号进程以外(0号进程是由系统创建的),任何一个进程都是由其他进程创建的。所以说 Linux中,1号进程是所有用户态进程的祖先,0号进程是所有内核线程的祖先。
-
fork、vfork和clone三个系统调用都可以创建一个新进程,而且都是通过调用do_fork来实现进程的创建;
Linux是通过复制父进程来创建一个新进程,进程创建的大致框架就是:复制PCB,对复制的PCB进行修改、分配新的内核堆栈...
3.1 fork函数
//函数原型 pid_t fork( void);
fork函数被调用一次但返回两次。两次返回的唯一区别是子进程中返回0值而父进程中返回子进程ID。
这个fork函数的最后的落脚点是do_fork
1703 SYSCALL_DEFINE0(fork) 1704 { 1705 #ifdef CONFIG_MMU 1706 return do_fork(SIGCHLD, 0, 0, NULL, NULL); 1707 #else 1708 /* can not support in nommu mode */ 1709 return -EINVAL; 1710 #endif 1711 }
3.2 clone过程分析
clone如果不管条件编译的内容,实际上也是执行了do_fork
1746 return do_fork(clone_flags, newsp, 0, parent_tidptr, child_tidptr);
所以分析从do_fork开始:按照老师所说的主要框架来找相关代码进行确认。
3.2.1 复制:
1651 p = copy_process(clone_flags, stack_start, stack_size, child_tidptr, NULL, trace);
copy_process中dup_task_struct函数复制了整个PCB。
1240 p = dup_task_struct(current);
3.2.2 对PCB进行修改
copy_process函数中,从dup_task_struct函数后面的都是对复制的PCB进行修改,包括初始化内存、文件系统、信号等等,其中理解的关键是:
1396 retval = copy_thread(clone_flags, stack_start, stack_size, p);
135 struct pt_regs *childregs = task_pt_regs(p); //SAVE_ALL地址 //修改SP 139 p->thread.sp = (unsigned long) childregs; 140 p->thread.sp0 = (unsigned long) (childregs+1); //还处于父进程中,将父进程SAVE_ALL的内容拷贝过来 159 *childregs = *current_pt_regs(); //修改IP,所以产生的子进程在系统调用处理过程中从ret_from_fork处开始执行。 164 p->thread.ip = (unsigned long) ret_from_fork;
3.2.3 ret_from_fork
程序接下来跳转到ret_from_fork。从此执行时内核堆栈只有之前存的一点点内容,中间的代码作用是怎么样的呢?
290 ENTRY(ret_from_fork) 291 CFI_STARTPROC 292 pushl_cfi %eax 293 call schedule_tail 294 GET_THREAD_INFO(%ebp) 295 popl_cfi %eax 296 pushl_cfi $0x0202 # Reset kernel eflags 297 popfl_cfi 298 jmp syscall_exit 299 CFI_ENDPROC 300 END(ret_from_fork)
可以查看 jmp syscall_exit,到syscall_exit时候,堆栈状态与系统调用前的是一样的。所以可以推断前面的292~297就是填充堆栈内容。
505 syscall_exit: 506 LOCKDEP_SYS_EXIT 507 DISABLE_INTERRUPTS(CLBR_ANY) # make sure we don't miss an interrupt 508 # setting need_resched or sigpending 509 # between sampling and the iret 510 TRACE_IRQS_OFF 511 movl TI_flags(%ebp), %ecx 512 testl $_TIF_ALLWORK_MASK, %ecx # current->work 513 jne syscall_exit_work
所以新创建的子进程获得cpu使用权时是从ret_from_fork开始执行,再返回到用户态,对应的是子进程的用户空间。
内容四:GBD验证分析过程
自己搭建的系统上调试。
3.1:按老师要求更改menu,并make rootfs
3.2:程序运行效果
3.3 GDB调试(图太多了,选了其中两张)
设置断点,并运行至第一个断点
运行到第二个断点,后面跟踪不到了。
内容五:小结
通过本次课的学习,自己掌握了如下的知识:
1:了解了进程的描述符,结构体 task_struct。对其中比较重要的声明有了一定的了解。
2:初步的学习了进程启动的流程。
2.1:通过fork创建一个新的进程,fork函数的特点是一次调用,两次返回。而其最终的落脚点是do_fork.
2.2: 在进程创建的过程中,通过copy_process中的dup_task_struct复制父进程的PCB,然后紧接着修改需要修改的内容。
2.3: 修改的内容有很多,其中很重要的一点是在copy_thread中,将子进程的IP设置为ret_from_fork。
2.4: 所以当子进程获得CPU的使用权时,子进程是从ret_from_fork这个标号处开始执行,执行一系列堆栈内容填充指令后,跳转到syscall_exit,最后切换
到用户态,此时则处于子进程的用户空间中。
3: 学习方法:linux的具体实现代码很多,细节也很多,如果直接看代码,很容易忽略主干,所以老师说,应该在看代码之前,思考并找出代码实现功能的基本框架,
然后在代码中找证据。