fork、vfork、clone之间的差异
fork/vfork/clone这三者均为系统调用,进程或线程通过这三种系统调用的某一种来创建子进程/线程,
这三种调用的底层实现均为linux内核源码kernel/fork.c中的do_fork()函数来实现,它们间的唯一的差别就是传入的参数值有差异。
*
* Ok, this is the main fork-routine.
*
* It copies the process, and if successful kick-starts
* it and waits for it to finish using the VM if required.
*/
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)
可看出do_fork有n个参数:(不同版本不一样)
(1)clone_flags:表示创建进程的各种标志位集合;
(2)stack_start:表示用户态栈的起始地址;
(3)stack_size:对应栈大小,通常设置为0;
(4)parent_tidptr和child_tidptr:分别指向用户态空间的两个指针,指向父进程和子进程的PID。
父进程通过系统调用创建子进程后,它们之间主要共享了哪些资源?
从源码的cloning flags中我们可以知道存在同一虚拟地址空间中的父子进程主要共享了4种资源:
(1)共享内存空间;(2)共享文件系统信息如根/当前工作目录等;(3)共享文件描述符;(4)共享相同的信号处理函数
/*
* cloning flags:
*/
#define CSIGNAL 0x000000ff /* signal mask to be sent at exit */
#define CLONE_VM 0x00000100 /* set if VM shared between processes */
#define CLONE_FS 0x00000200 /* set if fs info shared between processes */
#define CLONE_FILES 0x00000400 /* set if open files shared between processes */
#define CLONE_SIGHAND 0x00000800 /* set if signal handlers and blocked signals shared */
#define CLONE_PTRACE 0x00002000 /* set if we want to let tracing continue on the child too */
#define CLONE_VFORK 0x00004000 /* set if the parent wants the child to wake it up on mm_release */
#define CLONE_PARENT 0x00008000 /* set if we want to have the same parent as the cloner */
#define CLONE_THREAD 0x00010000 /* Same thread group? */
#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 */
#define CLONE_CHILD_CLEARTID 0x00200000 /* clear the TID in the child */
#define CLONE_DETACHED 0x00400000 /* Unused, ignored */
#define CLONE_UNTRACED 0x00800000 /* set if the tracing process can't force CLONE_PTRACE on this clone */
#define CLONE_CHILD_SETTID 0x01000000 /* set the TID in the child */
#define CLONE_NEWCGROUP 0x02000000 /* New cgroup namespace */
#define CLONE_NEWUTS 0x04000000 /* New utsname namespace */
#define CLONE_NEWIPC 0x08000000 /* New ipc namespace */
#define CLONE_NEWUSER 0x10000000 /* New user namespace */
#define CLONE_NEWPID 0x20000000 /* New pid namespace */
#define CLONE_NEWNET 0x40000000 /* New network namespace */
#define CLONE_IO 0x80000000 /* Clone io context */
fork实现:
- do_fork(SIGCHLD, 0, 0, NULL, NULL);
vfork实现:
- do_fork(CLONE_VFORK | CLONE_VM | SIGCHLD, 0, 0, NULL, NULL);
clone实现:
- do_fork(clone_flags, newsp, 0, parent_tidptr, child_tidptr);
参数SIGCHLD是子进程终止后用于通知父进程进行回收资源的,也就是说子进程终止后会发送SIGCHLD信号给父进程,父进程通过调用waitpid()或wait()函数回收子进程资源。
从上面fork和vfork的系统具体实现可以看出,vfork比fork多了2个标志位,分别是CLONE_VFORK和CLONE_VM;其中CLONE_VFORK表明父进程创建子进程后会被挂起,直到子进程结束释放了虚拟内存资源!
vfork创建一个线程,但主要用作创建进程的中间步骤。既然新的进程最终要调用execve执行新的目标程序,并与父进程分道扬镳,那么复制父进程的资源完全是多余的,于是vfork便应运而生,其创建一个线程并通过指针拷贝共享父进程的资源(这些资源会在execve中被替换)。 vfork与fork的另一个不同是,vfork会保证子进程先运行,在子进程执行execve或者exit之后,父进程才可能被调度运行;这也就是标志位CLONE_VM的功劳;具体使用场景就是就是 appinit进程 拉起各个业务进程使用vfork 后执行execv,等待子进程切换到别的路径执行,在执行父进程-------
而fork()系统调用会使得子进程建立一个基于父进程的完整副本,为了简便,子进程和父进程采用写时拷贝(copy-on-write,COW)机制,子进程只是拷贝了父进程的页表,并没有真正复制页表内容。只有当子进程需要写入新数据后才会采用COW为子进程创建一个副本。因此它们之间的完成先后并没有严格规定,就可能出现父进程或许先于子进程完成,或者晚于子进程完成的啦。
clone系统调用的功能:其功能主要是用于进程创建线程。三者区别显而易见,从clone的调用实参可以看出,可以指定子线程的用户空间堆栈的起始位置(newsp参数)-从用户空间传入内核空间,以及线程的入口。同时clone也可以用来创建一个进程,有选择性的复制父进程的资源(由参数flag来指定)---所以pthread的底层实现应该就是通过sys_clone系统调用完成的,具体要看代码哦!!
vfork会保证子进程先运行,在子进程执行execve或者exit之后,父进程才可能被调度运行。为了实现这个需求,task_struct结构中有一个struct completion *vfork_done; /* for vfork() */ 条件变量。在do_fork函数开始位置,。do_fork在完成子进程的“复制”以后,调用wake_up_process唤醒子进程,而自己则调用wait_for_vfork_done 进入睡眠。
/*
* Do this prior waking up the new thread - the thread pointer
* might get invalid after that point, if the thread exits quickly.
*/
if (!IS_ERR(p)) {
struct completion vfork;
struct pid *pid;
trace_sched_process_fork(current, p);
pid = get_task_pid(p, PIDTYPE_PID);
nr = pid_vnr(pid);
if (clone_flags & CLONE_PARENT_SETTID)
put_user(nr, parent_tidptr);
if (clone_flags & CLONE_VFORK) {
p->vfork_done = &vfork;
init_completion(&vfork);
get_task_struct(p);
}
wake_up_new_task(p);
/* forking complete and child started to run, tell ptracer */
if (unlikely(trace))
ptrace_event_pid(trace, pid);
if (clone_flags & CLONE_VFORK) {
if (!wait_for_vfork_done(p, &vfork))
ptrace_event_pid(PTRACE_EVENT_VFORK_DONE, pid);
}
put_pid(pid);
}
在子进程调用exec或者exit时,都会调用mm_release,在该函数中使用条件变量等待队列唤醒父进程。
void mm_release(struct task_struct *tsk, struct mm_struct *mm)
{
---------------------------
/*
* All done, finally we can wake up parent and return this mm to him.
* Also kthread_stop() uses this completion for synchronization.
*/
if (tsk->vfork_done)
complete_vfork_done(tsk);
}
仔细想来,vfork保证子进程先运行,不见得是内核“精心”为linux用户实现的特性,倒像是内核的无奈之举。我们知道vfork系统调用,父子进程共享进程用户空间,子进程对用户空间的修改会反过来影响父进程,反之亦然。如果说父子进程各自修改数据区的数据是危险的话,那么修改堆栈那就是致命的了。而恰巧子进程紧接着就会调用exec(或者exit),而函数调用涉及到传参,涉及到设置返回地址这些都会修改堆栈。所以决不能让两个进程都回到用户空间并发的运行,必须扣留其中的一个,而只让一个返回到用户空间去运行,直到两个进程不再共享用户空间(子进程执行exec)或者其中之一消亡(必然是先返回用户空间的子进程)为止。
即使如此,也还是有危险,子进程绝对不能从调用vfork的那个函数(caller)中返回(比如vfork之后子进程调用return),因为函数返回时,栈会被恢复平衡,之前用来存放caller的函数参数和函数返回地址的内存单元被回收,之后,这些内存单元可能被用来存放其他的内容。等到子进程去世父进程开始运行时,虽然父进程虽然有自己的esp(保存在pt_regs中),但栈中保存的caller的返回地址被破坏了(esp之上的内容都被破坏),这是很危险的。
所以vfork实际上是建立在子进程在创建后立即就会调用execve的前提之上的。
创建内核线程
内核启动过程中会调用start_kernel -->rest_init 调用函数 kernel_thread 创建 kernel_init 进程,也就是大名鼎鼎的 init 内核进程。 init 进程的 PID 为 1。
调用函数 kernel_thread 创建 kthreadd 内核进程,此内核进程的 PID 为 2。kthreadd 进程负责所有内核进程的调度和管理。
最后调用函数 cpu_startup_entry 来进入 idle 进程。当 CPU 没有事情做的时候就在 idle 空闲进程里面“瞎逛游”, idle 进程并没有使用 kernel_thread 或者 fork 函数来创建,因为它是有主进程演 变而来的。
void rest_init(void)
{
int pid;
………………
kernel_thread(kernel_init, NULL, CLONE_FS);
numa_default_policy();
pid = kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES);
rcu_read_lock();
kthreadd_task = find_task_by_pid_ns(pid, &init_pid_ns);
rcu_read_unlock();
complete(&kthreadd_done);//说明kthreadd已经创建成功了,并通过一个complete变量kthreadd_done来通知kernel_init线程
init_idle_bootup_task(current);初始化一个idle(闲置)进程,这个进程不做任何其他事情,只负责消耗时间片
schedule_preempt_disabled();设置这个进程是不会被调度。因为CPU显然利用率越高越好,不可能让调度程序调度一个只消耗时间片的进程
cpu_startup_entry(CPUHP_ONLINE);使得CPU在idle这样一个循环内进行工作,不断往复,从不返回
}
/*
* 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, 0);
}
kernel_thread实际实现也是通过 fo_fork 完成
看看kernel_init。 kernel_init既然是将要执行,我们就来看看kernel_init又会执行什么:
static int __ref kernel_init(void *unused)
{
int ret;
kernel_init_freeable();
/* need to finish all async __init code before freeing the memory */
async_synchronize_full();
free_initmem();
mark_rodata_ro();
system_state = SYSTEM_RUNNING;
numa_default_policy();
flush_delayed_fput();
if (ramdisk_execute_command) {
ret = run_init_process(ramdisk_execute_command);
if (!ret)
return 0;
pr_err("Failed to execute %s (error %d)\n",
ramdisk_execute_command, ret);
}
/*
* We try each of these until one succeeds.
*
* The Bourne shell can be used instead of init if we are
* trying to recover a really broken machine.
*/
if (execute_command) {
ret = run_init_process(execute_command);
if (!ret)
return 0;
panic("Requested init %s failed (error %d).",
execute_command, ret);
}
if (!try_to_run_init_process("/sbin/init") ||
!try_to_run_init_process("/etc/init") ||
!try_to_run_init_process("/bin/init") ||
!try_to_run_init_process("/bin/sh"))
return 0;
panic("No working init found. Try passing init= option to kernel. "
"See Linux Documentation/init.txt for guidance.");
}
kernel_init主要完成如下工作:
1、linux系统中每个进程都有自己的一个文件描述符表,表中存储的是本进程打开的文件。
2、linux系统中有一个设计理念:一切届是文件。所以设备也是以文件的方式来访问的。我们要访问一个设备,就要去打开这个设备对应的文件描述符。譬如/dev/fb0这个设备文件就代表LCD显示器设备,/dev/buzzer代表蜂鸣器设备,/dev/console代表控制台设备。打开一个设备的文件就会得到这个设备的文件描述符(或者是文件描述符的编号),这个编号就代表这个设备,以后操作这个设备就用这个文件描述符来操作它
3、这里我们打开了/dev/console文件,并且复制了2次文件描述符,一共得到了3个文件描述符。这三个文件描述符分别是0、1、2.这三个文件描述符就是所谓的:标准输入、标准输出、标准错误。
4、进程1打开了三个标准输出输出错误文件,因此后续的进程1衍生出来的所有的进程默认都具有这3个三件描述符
5、挂载根文件系统
6、执行用户态下的进程1程序;上面一旦挂载rootfs成功,则进入rootfs中寻找应用程序的init程序,找到后用run_init_process(里面的kernel_execve函数)去执行他!!
我们如何确定init程序是谁?
方法是:先从uboot传参cmdline中看有没有指定,如果有指定先执行cmdline中指定的程序。cmdline中的init=/linuxrc这个就是指定rootfs中哪个程序是init程序。这里的指定方式就表示我们rootfs的根目录下面有个名字叫linuxrc的程序,这个程序就是init程序。如果uboot传参cmdline中没有init=xx或者 cmdline中指定的这个xx执行失败,还有备用方案。
第一备用:/sbin/init,第二备用:/etc/init,第三备用:/bin/init,第四备用:/bin/sh
再不行就跪了!!!
int kthreadd(void *unused)
{
struct task_struct *tsk = current;
/* Setup a clean context for our children to inherit. */
set_task_comm(tsk, "kthreadd");
ignore_signals(tsk);
set_cpus_allowed_ptr(tsk, cpu_all_mask);
set_mems_allowed(node_states[N_MEMORY]);
current->flags |= PF_NOFREEZE;
for (;;) {
set_current_state(TASK_INTERRUPTIBLE);
if (list_empty(&kthread_create_list))
schedule();
__set_current_state(TASK_RUNNING);
spin_lock(&kthread_create_lock);
while (!list_empty(&kthread_create_list)) {
struct kthread_create_info *create;
create = list_entry(kthread_create_list.next,
struct kthread_create_info, list);
list_del_init(&create->list);
spin_unlock(&kthread_create_lock);
create_kthread(create);
spin_lock(&kthread_create_lock);
}
spin_unlock(&kthread_create_lock);
}
return 0;
}
- 第2号进程
kthreadd
进程由第0号进程通过kernel_thread()
创建,并始终运行在内核空间, 负责所有内核线程的调度和管理 - 第2号进程会循环检测
kthread_create_list
全局链表, 当我们调用kernel_thread
创建内核线程时,新线程会被加入到此链表中,因此所有的内核线程都是直接或者间接的以kthreadd为父进程 - 检测到新线程创建,则调用
kernel_thread()
创建线程,其回调为kthread
SO:
- 任何一个内核线程入口都是 kthread()
- 通过
kthread_create()
创建的内核线程不会立刻运行,需要手工 wake up
. - 通过
kthread_create()
创建的内核线程有可能不会执行相应线程函数threadfn
而直接退出
http代理服务器(3-4-7层代理)-网络事件库公共组件、内核kernel驱动 摄像头驱动 tcpip网络协议栈、netfilter、bridge 好像看过!!!!
但行好事 莫问前程
--身高体重180的胖子