结构体task_struct中部分成员的用法进行简要说明分析:
行号 成员 分析 1237 void *stack; //stack should points to a threadinfo struct 1239 unsigned int flags; //进程状态的信息 1244 int on_cpu; //当前进程在哪个CPU上运行 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; //实时任务调度实体 1272 unsigned int policy; //调度策略 1292 struct sched_info sched_info; //调度相关的信息(CPU上运行时间,队列等待时间等。) 1295 struct list_head tasks; //任务队列 1302 struct mm_struct *mm, *active_mm; //mm是进程的内存管理信息 1312 int exit_state; //进程退出时的状态 1313 int exit_code, exit_signal; //进程退出时发出的信号 1330 pid_t pid; //进程ID 1331 pid_t tgid; //线程组ID 1361 struct list_head thread_group; //该进程所有线程的链表 1381 u64 start_time; //线程启动时间 1384 u64 real_start_time; //线程启动时间 1401 int link_count, total_link_count; //文件系统信息计数 1412 struct thread_struct thread; //该进程在CPU下的状态 1414 struct fs_struct *fs; //文件系统信息结构体 1481 int lockdep_depth; //锁的深度 1488 void *journal_info; //文件系统日志信息 1491 struct bio_list *bio_list; //IO设备表 1624 unsigned long timer_slack_ns; //松弛时间值 1625 unsigned long default_timer_slack_ns; //松弛时间值Linux进程的状态与操作系统原理中的描述的进程状态有所不同,比如就绪状态和运行状态都是TASK_TUNNING,一个正在运行的进程,我们调用do_exit(),就进入了TASK_ZOMBIE(进程被终止)。程序创建的进程具有父子关系,在编程时往往需要引用这样的父子关系。进程描述符中有几个域用来表示这样的关系。
fork()是在用户态用于创建一个子系统的系统调用。Fork()系统调用在父进程和子进程各返回一次。即:
fork调用的一个奇妙之处就是它仅仅被调用一次,却能够返回两次,它可能有三种不同的返回值:
在执行fork函数后,如果新进程创建成功,则返回两个进程,一个是父进程,一个是子进程。在父进程中,fork返回新创建子进程的进程ID;在子进程中,fork函数返回0。我们可以通过输出fork返回的值来判断当前进程是子进程还是父进程。
fork出错可能有两种原因:
- 当前的进程数已经达到了系统规定的上限,这时errno的值被设置为EAGAIN;
- 系统内存不足,这时errno的值被设置为ENOMEM。
所谓道生一(start_kernel......cpu_idle),一生二(kernel_init和kthreadd),二生三(即前面0,1和2三个进程),三生万物(1号进程是所有用户态进程的祖先,2号进程是所有内核进程的祖先)。概述了系统启动,创建了第一个进程后,再分别创建用户态进程和内核态进程的过程。
在系统启动的时候,0号进程是我们手工写进入的,把它的系统描述符的数据结构用代码写死的。1号进程的创建相当于复制了一份0号进程的PCB,然后根据1号进程的需要,把PID等参数修改为1号进程的相应值。
2.2 fork()函数的底层实现
Linux通过复制父进程来创建一个新进程,那么这就给我们理解这一个过程提供一个想象的框架:
1. 复制一个PCB——task_struct
err = arch_dup_task_struct(tsk, orig);
2. 要给新进程分配一个新的内核堆栈
ti = alloc_thread_info_node(tsk, node);
tsk->stack = ti;
setup_thread_stack(tsk, orig); //这里只是复制thread_info,而非复制内核堆栈
3. 要修改复制过来的进程数据,比如pid、进程链表等等都要改改吧,见copy_process内部。
Linux通过clone()系统调用实现fork()。这个调用通过一系列的参数标志来指明父,子进程需要共享的资源。fork(),vfork()和__clone()库函数都根据各自需要的参数标志去调用clone()。然后由clone()去调用do_fork()。do_frok完成了创建中的大部分工作,它的定义在ker/frok.c文件中。该函数调用copy_process()的函数,然后让进程开始运行。copy_process()做扫尾工作并返回一个指向子进程的指针。再回到do_fork()函数,如果copy_process()函数返回成功,新创建的子进程被唤醒并让其投入运行。
3.1 使用实验楼环境进行试验,首先进入“LinuxKernel”文件夹,删除“menu”文件,重新下载“menu”文件,如下图所示:
3.2 进入“menu”文件中,利用mv命令将test.c替换为test_fork.c文件,如下图所示:
3.3 执行qemu命令,运行该程序:
3.4 进入gdb环境中,分别在sys_clone,do_fork,dup_task_struct,copy_process,copy_thread,ret_from_fork处设置断点,然后跟踪调试test_fork.c程序:
3.5单条指令开始执行,逐步跟踪do_fork的运行情况:
Linux中,fork()系统调用产生的子进程在系统调用处理过程中从ret_from_fork处开始执行。
Linux进程的创建过程就是内存中进程相关资源产生的过程,就是clone的过程。具体分析过程如下图:
分析:fork()函数创建一份子进程。首先是fork()函数执行系统调用,调用system_call(),system_call进而根据进程调用号找到sys_fork()函数所在的位置,进而执行sys_fork()函数;之后,sys_fork()函数开始调用do_fork()函数,do_frok()函数位于fork.c文件中,它完成了子进程创建的大部分工作;最后do_fork()函数调用copy_process()函数,copy_process()函数完成了很多工作,比如,为新进程分配内核堆栈,检查进程数目是否超限,区分父、子进程,获取子进程PID等操作。
参考资料: