一、前言
kernel在启动初期并没有“进程”这个概念,因为不涉及多任务并发、调度,kernel 起来后会在start_kernel
中创建kthread和init进程,在0号进程的基础上创建init进程(PID为1),0 进程会被设置成idle进程,加入到运行
队列中。当CPU上没有可调度进程时,调度器才会选择0号进程运行:
int start_kernel(){
......
kernel_thread(kernel_init, NULL, CLONE_FS);
kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES);
......
}
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, 0);
}
从上面的代码中可以看出,在start_kernel阶段会创建kthread和Init的进程,init进程是所有用户进程的父进程,
kthread进程是内核进程的父进程,下面会重点_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,
unsigned long tls)
1、写时复制技术(copy on write,COW):
进程的使用了写时拷贝技术,为了减少创建线程时的工作量,采用了写时复制技术,子进程只复制父进程的页表,
不会复制页面内容,当子进程需要写入新内容时才触发写时复制机制,为子进程创建一个副本。写时拷贝是一种可以
推迟甚至避免拷贝数据的技术。内核此时并不复制整个进程的地址空间,而是让父子进程共享同一个地址空间。只用
在需要写入的时候才会复制地址空间,从而使各个进行拥有各自的地址空间。
2、clone_flags标记位:
#define CSIGNAL 0x000000ff /* signal mask to be sent at exit */
#define CLONE_VM 0x00000100 /* 父子进程共享内存空间 */
#define CLONE_FS 0x00000200 /* 父子进程共享文件系统 */
#define CLONE_FILES 0x00000400 /* 父子进程共享文件描叙符 */
#define CLONE_SIGHAND 0x00000800 /* 父子进程共享相同的信号处理 */
#define CLONE_PTRACE 0x00002000 /* 父进程被trace,子进程同样被trace */
#define CLONE_VFORK 0x00004000 /* 父进程被挂起,直到子进程释放了虚拟内存资源(exit/execve) */
#define CLONE_THREAD 0x00010000 /* 父子进程共享相同的线程群 */
#define CLONE_NEWNS 0x00020000 /* New mount namespace group */
#define CLONE_SYSVSEM 0x00040000 /* share system V SEM_UNDO semantics */
#define CLONE_SETTLS 0x00080000 /* create a new TLS for the child */
#define CLONE_PARENT_SETTID 0x00100000 /* set the TID in the parent */
3、其中do_fork所做的工作可以用下图来初步的描叙:
(1)do_fork主要调用copy_process()函数来创建一个新的进程,copy_process函数非常长,可以分解为如下:
a、标志位clone_flags检查:
- CLONE_NEWNS表示父子进程不共享mount namespace,CLONE_FS父子进程共享文件系统,两则不能同时使用;
- CLONE_THREAD表示进程在同一个线程组里,CLONE_SIGHAND表示父子进程共享相同的信号处理表;
- CLONE_PARENT表示新建的进程是兄弟进程,而不是父子进程;
- CLONE_NEWPID表示一个新的PID namespace;
b、dump_task_struct( )分配task_struct:
- struct task_struct是描叙进程的核心数据结构,用于描叙进程的状态信息和控制信息;
- struct thread_info用于村粗描叙符频繁访问和硬件快速访问的字段;
- tsk = alloc_task_struct_node(node)通过slub先分配一个task_struct的结构体的内存;
- stack = alloc_thread_stack_node(tsk, node) 分配内核栈所用的内存(32bit为8kB,64bit为16KB),此块内存每创建一个用户进程都会申请对应空间的大小:
static unsigned long *alloc_thread_stack_node(struct task_struct *tsk, int node) { //#define THREAD_SIZE_ORDER 1 ===> 32bit为1,申请2 * 4kB = 8kB的大小 //#define THREAD_SIZE_ORDER 2 ===> 64bit为2,申请4 * 4kB = 16KB的大小 struct page *page = alloc_kmem_pages_node(node, THREADINFO_GFP, THREAD_SIZE_ORDER); return page ? page_address(page) : NULL; }
-
arch_dup_task_struct(tsk, orig)拷贝父进程的task_struct数据结构的内容到子进程中;
-
setup_thread_stack(tsk, orig)拷贝父进程的thread_info数据结构的内容到子进程中;
c、sched_fork(clone_flags, p)初始化进程调度相关的数据结构,如调度实体;
d、复制父进程的信息:
- copy_files(clone_flags, p)复制父进程的打开的文件等信息;
- copy_fs(clone_flags, p)复制父进程fs_struct结构的信息;
- copy_sighand(clone_flags, p)复制父进程的信号系统;
- copy_io(clone_flags, p)复制父进程的IO相关信息;
- copy_mm(clone_flags, p)复制父进程的内存空间,这里只会复制pte页表项,并没有复制VMA对应的页面的内容(写时复制);
(2)在copy_proces()函数中成功创建一个进程后,会调用wake_up_new_task(p)将对应的进程加入
调度器中等待调度执行。
作者:frank_zyp
您的支持是对博主最大的鼓励,感谢您的认真阅读。
本文无所谓版权,欢迎转载。