Skip to content

Latest commit

 

History

History
337 lines (309 loc) · 15.1 KB

linux的进程诞生史.md

File metadata and controls

337 lines (309 loc) · 15.1 KB

进程诞生学习小记

这个玩意不亚于开天辟地,因此必须要重头说起

我们只从内核加载开始说起,那么从实际代码上,一切的一切都是从start_kernel()开始的,这个代码是内核真正的初始化过程,使得一个完整的linux内核环境被建立起来。(而在此之前的初始化只是为了能够让内核程序最低限度的执行初始化操作)

只管和进程创建有关的

我的是v4.15内核,第一个关注到的操作就是这一行代码:

set_task_stack_end_magic(&init_task);

先看一下函数的实现,来源于fork.c

void set_task_stack_end_magic(struct task_struct *tsk)
{
 unsigned long *stackend;
 stackend = end_of_stack(tsk);
 *stackend = STACK_END_MAGIC; /* for overflow detection */
}

end_of_stack呢在task_stack.h里面,非常简单的一个函数,就是返回内核栈边界地址

static inline unsigned long *end_of_stack(struct task_struct *p)
{
#ifdef CONFIG_STACK_GROWSUP
 return (unsigned long *)((unsigned long)task_thread_info(p) + THREAD_SIZE) - 1;
#else
 return (unsigned long *)(task_thread_info(p) + 1);
#endif
}

接着set_task_stack_end_magic会把栈底地址设置成STACK_END_MAGIC作为栈溢出的标志。 这不是什么问题,主要来看下set_task_stack_end_magic的传参&init_task,它在init_task.c中被初始化:

struct task_struct init_task = INIT_TASK(init_task);
EXPORT_SYMBOL(init_task);

最终就定位到了INIT_TASK,这可以理解成一个手动初始化出来的进程结构。看源码的话也能很轻易的看出是一个task_struct的数据结构。 至此第一个task_struct就现身了,也就是init_task,但此刻的它还只是一个进程描述符,还需要运行起来。

 /*
  * Set up the scheduler prior starting any interrupts (such as the
  * timer interrupt). Full topology setup happens at smp_init()
  * time - but meanwhile we still have a functioning scheduler.
  */
 sched_init();

这个函数初始化了各种调度相关的数据结构,其中有一条代码:

 /*
  * Make us the idle thread. Technically, schedule() should not be
  * called from this thread, however somewhere below it might be,
  * but because we are the idle thread, we just pick up running again
  * when this runqueue becomes "idle".
  */
 init_idle(current, smp_processor_id());

而此时的current就是&init_task,再跟入看下

__sched_fork(0, idle);

至此init_task被初始化成0号进程也就是idle进程进入到了cpu运行队列中。

init_taskidle的进程描述符,也是整个内核第一个进程描述符,他是被静态创建的。

继续跟下去,就来到了start_kernel的结尾rest_init();,这个函数的统一说法是什么呢?

这个函数的主要使命就是创建并启动内核线程init。

跟进去看,跳过第一步的RCU锁机制启动函数

 /*
  * We need to spawn init first so that it obtains pid 1, however
  * the init task will end up wanting to create kthreads, which, if
  * we schedule it before we create kthreadd, will OOPS.
  */
 pid = kernel_thread(kernel_init, NULL, CLONE_FS);

注释的意思是说:

我们必须先创建一个init线程这样它就能获得pid=1,虽然init线程会挂起来等待kthreads创建,如果我们提前调度init就会导致oops。

先看一下kernel_thread

/*
 * 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);
}

那简单了,pid=1的人找到了,就是kernel_init。当然还没几行:

pid = kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES);

至此linux中的前三个进程产生了,说一说作用:

* idle进程由系统自动创建, 运行在内核态 
idle进程其pid=0,其前身是系统创建的第一个进程,也是唯一一个没有通过fork或者kernel_thread产生的进程。完成加载系统后,演变为进程调度、交换

* init进程由idle通过kernel_thread创建,在内核空间完成初始化后, 加载init程序, 并最终用户空间 
由0进程创建,完成系统的初始化. 是系统中所有其它用户进程的祖先进程 
Linux中的所有进程都是有init进程创建并运行的。首先Linux内核启动,然后在用户空间中启动init进程,再启动其他系统进程。在系统启动完成完成后,init将变为守护进程监视系统其他进程。

* kthreadd进程由idle通过kernel_thread创建,并始终运行在内核空间, 负责所有内核线程的调度和管理 
它的任务就是管理和调度其他内核线程kernel_thread, 会循环执行一个kthread的函数,该函数的作用就是运行kthread_create_list全局链表中维护的kthread, 当我们调用kernel_thread创建的内核线程会被加入到此链表中,因此所有的内核线程都是直接或者间接的以kthreadd为父进程 
  • 统一来说就是,init是所有用户态进程的祖先,kthreadd是所有内核线程的祖先。

这儿先抛出一个知识:Linux上进程分3种,内核线程(或者叫核心进程)、用户进程、用户线程。

说完了上述的三个进程,就该说说其余进程的创建了。 Linux提供了三种创建进程的方式:

  1. fork
  2. vfork
  3. clone

但是这三种方式归根到底都是使用的do_fork来实现,而核心原理也是通过复制task然后做处理。 但是能接触到的实际都是用户态进程,因此还需要继续把上述的过程往用户态引申,可以看下ps -aux的执行结果,systemdPID为1的进程,而这个进程的command/sbin/init,那这个到底是什么时候执行的呢?

  • 答案是是在kernel加载之后,结束之前。

complete(&kthreadd_done);这个操作通知了kernel_init已经完成了kthreadd的创建后,首先是rest_initschedule_preempt_disabled();,它的注释是这样的:

 /*
  * The boot idle thread must execute schedule()
  * at least once to get things moving:
  */

就是说为了让系统跑起来,boot idle至少执行一次schedule(),执行之后kernel_initkthreadd就也同时运行起来了。

这部分的运行机制需要涉及到调度系统的知识,总的来说就是全局考评,确定需要执行的进程。

rest_init的最后一个操作是cpu_startup_entry(CPUHP_ONLINE);,而这个操作是这样的:

void cpu_startup_entry(enum cpuhp_state state)
{
 /*
  * This #ifdef needs to die, but it's too late in the cycle to
  * make this generic (arm and sh have never invoked the canary
  * init for the non boot cpus!). Will be fixed in 3.11
  */
#ifdef CONFIG_X86
 /*
  * If we're the non-boot CPU, nothing set the stack canary up
  * for us. The boot CPU already has it initialized but no harm
  * in doing it again. This is a good place for updating it, as
  * we wont ever return from this function (so the invalid
  * canaries already on the stack wont ever trigger).
  */
 boot_init_stack_canary();
#endif
 arch_cpu_idle_prepare();
 cpuhp_online_idle(state);
 while (1)
  do_idle();
}

可以看到最后在无限循环一个do_idle(),内核会进入到idle状态,循环消耗空闲的cpu时间片,当有其他进程需要工作时候,就会被抢占,至此整个内核就运行结束了。 有个资料总结的就挺好的:

简单来说,linux内核最终的状态是:有事干的时候去执行有意义的工作(执行各个进程任务),实在没活干的时候就去死循环(实际上死循环也可以看成是一个任务)。

kernel_init

这时候就要说到kernel_init了,进入到源码中,就能很轻易的看到针对/sbin/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();
 ftrace_free_init_mem();
 free_initmem();
 mark_readonly();
 system_state = SYSTEM_RUNNING;
 numa_default_policy();

 rcu_end_inkernel_boot();

 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/admin-guide/init.rst for guidance.");
}

前半部分都不太需要管,而这儿就要注意两个函数run_init_processtry_to_run_init_processtry_to_run_init_process是调用的run_init_process,而最终调用的是do_execve,这是来加载运行可执行程序的函数。 而从上到下可能执行的部分有:

  1. ramdisk_execute_command
  2. execute_command
  3. 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")

第一和第二都是由内核启动参数来决定的,分别是rdinit=init=

static int __init init_setup(char *str)
{
 unsigned int i;

 execute_command = str;
 /*
  * In case LILO is going to boot us with default command line,
  * it prepends "auto" before the whole cmdline which makes
  * the shell think it should execute a script with such name.
  * So we ignore all arguments entered _before_ init=... [MJ]
  */
 for (i = 1; i < MAX_INIT_ARGS; i++)
  argv_init[i] = NULL;
 return 1;
}
__setup("init=", init_setup);

static int __init rdinit_setup(char *str)
{
 unsigned int i;

 ramdisk_execute_command = str;
 /* See "auto" comment in init_setup */
 for (i = 1; i < MAX_INIT_ARGS; i++)
  argv_init[i] = NULL;
 return 1;
}
__setup("rdinit=", rdinit_setup);

倘若都没有设置的话,就会执行到硬编码路径的执行程序,按照顺序依次如下:

/sbin/init
/etc/init
/bin/init
/bin/sh

如果什么都没有的话,就进入kernel panic。 那按照正常流程下,整个系统运行的第一个用户态可执行程序就是/sbin/init了,而因为是通过do_execve装载的程序,PID1的进程的代码段被替换成新程序的代码段,而原有的数据段堆栈段则被放弃,然后重新分配,唯一保留的就还是PID了,那此刻整个系统中PID=1的已经从kernel_init这个在内核态运行的内核代码变成了/sbin/init这个用户态的进程。

PID=1:
kernel_init => /sbin/init

至此,第一个用户态进程也就正式出现了,以前叫init,现在叫systemd

用户态下的新进程

那现在看一下,用户态下一个新程序运行的话,其进程的创建过程是什么

就以fork来说:

p = copy_process(clone_flags, stack_start, stack_size,
    child_tidptr, NULL, trace, tls, NUMA_NO_NODE);

会拷贝一份进程信息,内核栈thread_info与父进程相同。接着根据这个新描述符获取到一个pid,其中关于pid的诞生:

pid = alloc_pid(p->nsproxy->pid_ns_for_children);

alloc_pid子进程描述符分配了对应的描述符号

此时子进程要使用的pid就诞生了

接着就是从子进程描述符中获取pid

pid = get_task_pid(p, PIDTYPE_PID);

这就值得玩味了?这个pid到底是一个什么获取法? get_task_pid(struct tast_struct *task) => if (type != PIDTYPE_PID){task = task->group_leader;} => get_pid(task->pids[type].pid) => atomic_inc(&pid->count); atomic_inc是个原子操作函数,作用呢就是原子变量值加一,效果就是把count+1,再接着就是一个获取子进程nr值的操作:

nr = pid_vnr(pid);
.........pid_nr_ns.........
nr = upid->nr

那此刻就得知道下pid的结构了:

struct pid
{
 atomic_t count;
 unsigned int level;
 /* lists of tasks that use this pid */
 struct hlist_head tasks[PIDTYPE_MAX];
 struct rcu_head rcu;
 struct upid numbers[1];
};
  1. count是引用计数器
  2. level是该进程的命名空间在命名空间层次结构中的深度
  3. numbers是一个upid实例的数组,每个数组对应一个命名空间(表现上只有一个,但是可以扩展)
  4. tasks是共享此pid的所有进程的链表表头,其中的进程通过pids[type]成员构链接。

后面就是返回nr值了,那也就是说,实际上最重要的部分在于copy_process

该函数会用当前进程的一个副本来创建新进程并分配pid,但不会实际启动这个新进程。它会复制寄存器中的值、所有与进程环境相关的部分,每个clone标志。新进程的实际启动由调用者来完成。

那么就是一个do_fork就必然会产生一个PID,至于用不用两说。

namespcae

虚拟化的东西,很难理解,先放着吧。。。。

参考资料