描述进程的数据结构
在Linux内核中使用task_struct这个结构体来描述进程。完整代码可以见详细代码,task_struct结构非常复杂,里面有进程的状态,进程双向链表管理等等。
以下为Linux内核管理进程状态转换图
进程有五个状态,进程的创建-进程的就绪状态-进程的阻塞状态-进程的运行状态-进程的终止状态。
首先使用fork方法来创建一个子进程,注意这里有两个TASK_RUNNING但不同的是一个是就绪,一个是运行,请注意那段running on CPU也就是说在CPU中执行的就是运行态,被内核调度出去,在等待队列中的就是就绪态。如果等待某些特定资源就会进入阻塞态,在TASK_INTERRUPTIBLE时可以被信号和wake_up()唤醒,进程可变为TASK_RUNNING,如果是TASK_UNINTERRUPTIBLE,只能被wake_up()唤醒。
在task_struct中state,stack和双向链表(把所有进程用双向链表连接起来)都很重要。
分析fork
对于fork的完整代码可见代码,fork是一个系统调用,实际就是把当前进程又复制一个子进程,一变二,所有使用n次fork就会有2的n次方个进程。注意父进程和子进程的返回值是不同的。
/*
* Create a kernel thread.
*/
pid_t kernel_thread(int (*fn)(void *), void *arg, unsigned long flags)
{
return do_fork(flags|CLONE_VM|CLONE_UNTRACED, (unsigned long)fn,
(unsigned long)arg, NULL, NULL);
}
#ifdef __ARCH_WANT_SYS_FORK
SYSCALL_DEFINE0(fork)
{
#ifdef CONFIG_MMU
return do_fork(SIGCHLD, 0, 0, NULL, NULL);
#else
/* can not support in nommu mode */
return -EINVAL;
#endif
}
#endif
#ifdef __ARCH_WANT_SYS_VFORK
SYSCALL_DEFINE0(vfork)
{
return do_fork(CLONE_VFORK | CLONE_VM | SIGCHLD, 0,
0, NULL, NULL);
}
#endif
#ifdef __ARCH_WANT_SYS_CLONE
#ifdef CONFIG_CLONE_BACKWARDS
SYSCALL_DEFINE5(clone, unsigned long, clone_flags, unsigned long, newsp,
int __user *, parent_tidptr,
int, tls_val,
int __user *, child_tidptr)
#elif defined(CONFIG_CLONE_BACKWARDS2)
SYSCALL_DEFINE5(clone, unsigned long, newsp, unsigned long, clone_flags,
int __user *, parent_tidptr,
int __user *, child_tidptr,
int, tls_val)
#elif defined(CONFIG_CLONE_BACKWARDS3)
SYSCALL_DEFINE6(clone, unsigned long, clone_flags, unsigned long, newsp,
int, stack_size,
int __user *, parent_tidptr,
int __user *, child_tidptr,
int, tls_val)
#else
SYSCALL_DEFINE5(clone, unsigned long, clone_flags, unsigned long, newsp,
int __user *, parent_tidptr,
int __user *, child_tidptr,
int, tls_val)
#endif
{
return do_fork(clone_flags, newsp, 0, parent_tidptr, child_tidptr);
}
#endif
可以从以上代码中看出fork,vfork和clone三个系统调用(System)以及内核函数kernel_thread都有一个return do_fork即都和do_fork有关。对于do_fork可以见
在do_fork参数中有一个clone_flags是子进程创建相关标志,通过这个标志对父进程资源进行有选择复制(比如子程序用户态堆栈地址,指向pt_regs结构体的指针,用户态栈大小以及父子进程用户态下pid地址等)。
do_fork主要是为了完成调用copy_process()来复制父进程信息,获得pid,调用wake_up_new_task将子进程进入等待队列,并获得CPU分配的资源,通过clone_flags标志做一些辅助工作。
copy_process函数为了完成调用dup_task_struct,来复制父进程描述符task_struct,信息检查,初始化,把进程状态复制为TASK_RUNNING(子进程置为就绪态),采用写时复制逐一复制所有其他进程资源,调用copy_thread初始化子进程内核栈,设置子进程pid等。总的来说dup_task_struct就是复制父进程描述符task_struct,pid等,copy_thread初始化子进程内核栈。
对于copy_thread代码详见代码
也就是说进程的创建过程就是
1.复制进程描述符(task_struct)
2.通过写时复制一一复制其他进程资源
3.分配子进程内核堆栈
4.对内核堆栈关键信息进行初始化
fork()实际为sys_clone()
使用gdb调试
首先git clone一个内核
git clone https://github.com/mengning/menu.git
然后进入menu文件夹,将test_fork.c替代test.c,可以使用命令mv,我们可以进入test_fork.c可以看到一个Fork函数,里面有wait和fork,逻辑应该很简单
以防报错,最好把TimeAsm函数以及main函数里MenuConfig关于TimeAsm的一行注释掉。
然后输入make rootfs指令。
再开一个端口
cd ~/linux-5.0.1
gdb
file vmlinux
target remote:1234
设置断点sys_clone,do_fork,copy_process,dup_task_struct,copy_thread和ret_from_fork。
这里无法设置sys_clone断点,暂不知道原因。
可以看到do_fork函数里有copy_process函数。
总结
这次学习了Linux中进程的创建过程,Linux进程创建依托于fork函数,fork实际就是调用系统调用函数sys_clone。进程创建大致有四个步骤,第一是复制父进程的进程标志符(task_struct),第二一一复制其他进程的资源,第三创建子进程的内核栈,第四对子进程的内核栈中关键信息进行初始化。和创建进程关联很大的函数有do_fork(),copy_process,dup_task_struct,copy_thread和ret_form_fork。