上电后首先通过汇编指令去加载uboot引导程序,然后由uboot从分区中加载内核镜像等,并启动内核。本文将从启动内核开始分析,对于汇编启动的过程,此处不做分析,有兴趣的可以自行研究。
Linux内核启动主要涉及3个特殊的进程,idle进程(PID = 0), init进程(PID = 1)和kthreadd进程(PID = 2),这三个进程是内核的基础。
idle进程是Linux系统第一个进程,是init进程和kthreadd进程的父进程
init进程是Linux系统第一个用户进程,是Android系统应用程序的始祖,我们的app都是直接或间接以它为父进程
kthreadd进程是Linux系统内核管家,所有的内核线程都是直接或间接以它为父进程
idle进程的启动是用汇编语言写的,对应文件是kernel/msm-4.4/arch/arm64/kernel/head.S,因为都是用汇编语言写的,我就不多介绍了,截取其中的关键部分如下:
str22, [x4] // Save processor ID
str x21, [x5] // Save FDT pointer
str x24, [x6] // Save PHYS_OFFSET
mov x29, #0
b start_kernel //跳转start_kernel函数
其中语句b start_kernel,b 是跳转的意思,跳转到start_kernel.h,这个头文件对应的实现在kernel/msm-4.4/init/main.c
asmlinkage __visible void __init start_kernel(void)
{
......
rest_init();
}
start_kernel函数在最后会调用rest_init函数,这个函数开启了init进程和kthreadd进程,下面就着重分析下rest_init函数。
定义在kernel/msm-4.4/init/main.c中
/*
* C语言oninline与inline是一对意义相反的关键字,inline的作用是编译期间直接替换代码块;oninline作用是强制不替换,保持原有的函数
* __init_refok是__init的扩展,__init定义的初始化函数会放入名叫.init.text的输入段,当内核启动完毕后,这个段中的内存会被释放掉,在本文中有讲,关注3.5 free_initmem。
*/
static noinline void __init_refok rest_init(void)
{
int pid;
//启动RCU机制,这个与后面的rcu_read_lock和rcu_read_unlock是配套的,用于多核同步
rcu_scheduler_starting();
smpboot_thread_init();
/*
* 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.
*/
/* 函数名既可以表示函数,也可以表示函数指针;kernel_init却作为参数传递了过去,其实传递过去的是一个函数指针
* 用kernel_thread方式创建init进程
* CLONE_FS 子进程与父进程共享相同的文件系统,包括root、当前目录、umask,CLONE_SIGHAND 子进程与父进程共享相同的信号处理(signal handler)表
*/
kernel_thread(kernel_init, NULL, CLONE_FS);
// 设定NUMA系统的默认内存访问策略
numa_default_policy();
//用kernel_thread方式创建kthreadd进程,CLONE_FILES 子进程与父进程共享相同的文件描述符(file descriptor)表
pid = kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES);
//打开RCU读取锁,在此期间无法进行进程切换
rcu_read_lock();
// 获取kthreadd的进程描述符,期间需要检索进程pid的使用链表,所以要加锁
kthreadd_task = find_task_by_pid_ns(pid, &init_pid_ns);
//关闭RCU读取锁
rcu_read_unlock();
// complete和wait_for_completion是配套的同步机制,跟java的notify和wait差不多,
// 之前kernel_init函数调用了wait_for_completion(&kthreadd_done),这里调用complete就是通知kernel_init进程kthreadd进程已创建完成,可以继续执行
complete(&kthreadd_done);
/*
* The boot idle thread must execute schedule()
* at least once to get things moving:
*/
//current表示当前进程,当前0号进程init_task设置为idle进程
init_idle_bootup_task(current);
//0号进程主动请求调度,让出cpu,1号进程kernel_init将会运行,并且禁止抢占
schedule_preempt_disabled();
/* Call into cpu_idle with preempt disabled */
// 这个函数会调用cpu_idle_loop()使得idle进程进入自己的事件处理循环
cpu_startup_entry(CPUHP_ONLINE);
}
rest_init的字面意思是剩余的初始化,但是它却一点都不剩余,它创建了Linux系统中两个重要的进程init和kthreadd,并且将init_task进程变为idle进程,接下来我将把rest_init中的方法逐个解析,方便大家理解。
定义在kernel/msm-4.4/kernel/rcu/Tree.c
void rcu_scheduler_starting(void)
{
WARN_ON(num_online_cpus() != 1); //WARN_ON相当于警告,会打印出当前栈信息,不会重启,num_online_cpus表示当前启动的cpu数
WARN_ON(nr_context_switches() > 0); // nr_context_switches 进行进程切换的次数
rcu_scheduler_active = 1; //启用rcu机制
}
定义在kernel/msm-4.4/kernel/fork.c
/*
* C语言中 int (*fn)(void *)表示函数指针的定义,int是返回值,void是函数的参数,fn是名字
*
*/
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);
}
do_fork函数用于创建进程,它首先调用copy_process()创建新进程,然后调用wake_up_new_task()将进程放入运行队列中并启动新进程。kernel_thread的第一个参数是一个函数指针,会在创建进程后执行,第三个参数是创建进程的方式,具体如下:
参数名 | 作用 |
---|---|
CLONE_PARENT | 创建的子进程的父进程是调用者的父进程,新进程与创建它的进程成了“兄弟”而不是“父子” |
CLONE_FS | 子进程与父进程共享相同的文件系统,包括root、当前目录、umask |
CLONE_FILES | 子进程与父进程共享相同的文件描述符(file descriptor)表 |
CLONE_NEWNS | 在新的namespace启动子进程,namespace描述了进程的文件hierarchy |
CLONE_SIGHAND | 子进程与父进程共享相同的信号处理(signal handler)表 |
CLONE_PTRACE | 若父进程被trace,子进程也被trace |
CLONE_UNTRACED | 若父进程被trace,子进程不被trace |
CLONE_VFORK | 父进程被挂起,直至子进程释放虚拟内存资源 |
CLONE_VM | 子进程与父进程运行于相同的内存空间 |
CLONE_PID | 子进程在创建时PID与父进程一致 |
CLONE_THREAD | Linux 2.4中增加以支持POSIX线程标准,子进程与父进程共享相同的线程群 |
定义在kernel/msm-4.4/init/main.c;这个函数比较重要,负责init进程的启动,这个过程将放在后面重点分析。
定义在kernel/msm-4.4/mm/mempolicy.c
/* Reset policy of current process to default */
void numa_default_policy(void)
{
do_set_mempolicy(MPOL_DEFAULT, 0, NULL); //设定NUMA系统的内存访问策略为MPOL_DEFAULT
}
定义在kernel/msm-4.4/kernel/kthread.c中;kthreadd进程将在第二篇中重点讲,它是内核中重要的进程,负责内核线程的调度和管理,内核线程基本都是以它为父进程的
定义在kernel/msm-4.4/include/linux/rcupdate.h中;RCU(Read-Copy Update)是数据同步的一种方式,在当前的Linux内核中发挥着重要的作用。RCU主要针对的数据对象是链表,目的是提高遍历读取数据的效率,为了达到目的使用RCU机制读取数据的时候不对链表进行耗时的加锁操作。这样在同一时间可以有多个线程同时读取该链表,并且允许一个线程对链表进行修改(修改的时候,需要加锁)
static inline void rcu_read_lock(void)
{
__rcu_read_lock();
__acquire(RCU);
rcu_lock_acquire(&rcu_lock_map);
rcu_lockdep_assert(!rcu_is_cpu_idle(),
"rcu_read_lock() used illegally while idle");
}
static inline void rcu_read_unlock(void)
{
rcu_lockdep_assert(!rcu_is_cpu_idle(),
"rcu_read_unlock() used illegally while idle");
rcu_lock_release(&rcu_lock_map);
__release(RCU);
__rcu_read_unlock();
}
定义在kernel/msm-4.4/kernel/pid.c中
task_struct叫进程描述符,这个结构体包含了一个进程所需的所有信息,它定义在kernel/msm-4.4/include/linux/sched.h文件中。
它的结构十分复杂,本文就不重点讲了,可以参考Linux进程描述符task_struct结构体详解
/*
* Must be called under rcu_read_lock().
*/
struct task_struct *find_task_by_pid_ns(pid_t nr, struct pid_namespace *ns)
{
rcu_lockdep_assert(rcu_read_lock_held(),
"find_task_by_pid_ns() needs rcu_read_lock()"
" protection"); //必须进行RCU加锁
return pid_task(find_pid_ns(nr, ns), PIDTYPE_PID);
}
struct pid *find_pid_ns(int nr, struct pid_namespace *ns)
{
struct upid *pnr;
hlist_for_each_entry_rcu(pnr,
&pid_hash[pid_hashfn(nr, ns)], pid_chain)
if (pnr->nr == nr && pnr->ns == ns)
return container_of(pnr, struct pid,
numbers[ns->level]); //container_of()的作用就是通过一个结构变量中一个成员的地址找到这个结构体变量的首地址
return NULL;
}
struct task_struct *pid_task(struct pid *pid, enum pid_type type)
{
struct task_struct *result = NULL;
if (pid) {
struct hlist_node *first;
first = rcu_dereference_check(hlist_first_rcu(&pid->tasks[type]),
lockdep_tasklist_lock_is_held());
if (first)
result = hlist_entry(first, struct task_struct, pids[(type)].node); //从hash表中找出struct task_struct
}
return result;
}
find_task_by_pid_ns的作用就是根据pid,在hash表中获得对应pid的task_struct
定义在kernel/msm-4.4/kernel/sched/core.c中
void __cpuinit init_idle_bootup_task(struct task_struct *idle)
{
idle->sched_class = &idle_sched_class; //设置进程的调度器类为idle_sched_class
}
Linux依据其调度策略的不同实现了5个调度器类, 一个调度器类可以用一种或者多种调度策略调度某一类进程, 也可以用于特殊情况或者调度特殊功能的进程.其所属进程的优先级顺序为stop_sched_class -> dl_sched_class -> rt_sched_class -> fair_sched_class -> idle_sched_class
可见idle_sched_class的优先级最低,只有系统空闲时才调用idle进程
定义在kernel/msm-4.4/kernel/sched/core.c中
/**
* schedule_preempt_disabled - called with preemption disabled
*
* Returns with preemption disabled. Note: preempt_count must be 1
*/
void __sched schedule_preempt_disabled(void)
{
sched_preempt_enable_no_resched(); //开启内核抢占
schedule(); // 并主动请求调度,让出cpu
preempt_disable(); // 关闭内核抢占
}
定义在kernel/msm-4.4/kernel/cpu/idle.c中
void cpu_startup_entry(enum cpuhp_state state)
{
#ifdef CONFIG_X86
boot_init_stack_canary();//只有在x86这种non-boot CPU机器上执行,该函数主要用于初始化stack_canary的值,用于防止栈溢出
#endif
arch_cpu_idle_prepare(); //进行idle前的准备工作
cpu_idle_loop(); //进入idle进程的事件循环
}
idle进程是Linux系统的第一个进程,进程号是0,在完成系统环境初始化工作之后,开启了两个重要的进程,init进程和kthreadd进程,执行完创建工作之后,开启一个无限循环,负责进程的调度。下一篇文章中我将以kthreadd为入口,讲解Linux系统内核管家之kthreadd进程,未完待续。。。