Init |
Kthreadd |
Migration |
ksoftirqd |
Watchdogd |
Events |
Init
Linux下有3个特殊的进程,idle进程(PID=0), init进程(PID=1)和kthreadd(PID=2)
* idle进程由系统自动创建,运行在内核态
idle进程其pid=0,其前身是系统创建的第一个进程,也是唯一一个没有通过fork或者kernel_thread产生的进程。完成加载系统后,演变为进程调度、交换
* kthreadd进程由idle通过kernel_thread创建,并始终运行在内核空间,负责所有内核线程的调度和管理
kthreadd (pid = 2, ppid = 0)
它的任务就是管理和调度其他内核线程kernel_thread,会循环执行一个kthread的函数,该函数的作用就是运行kthread_create_list全局链表中维护的kthread,当我们调用kernel_thread创建的内核线程会被加入到此链表中,因此所有的内核线程都是直接或者间接的以kthreadd为父进程
* init进程由idle通过kernel_thread创建,在内核空间完成初始化后,加载init程序,并最终用户空间创建
init 进程 (pid = 1, ppid = 0)
init进程由0进程创建,完成系统的初始化.是系统中所有其它用户进程的祖先进程
Linux中的所有进程都是有init进程创建并运行的。首先Linux内核自行启动(已经被载入内存,开始运行,并已初始化所有的设备驱动程序和数据结构等)之后,然后在用户空间中启动init进程,完成引导进程的启动。在系统启动完成后,init将变为守护进程监视系统其他进程,所以,init始终是第一个进程(其进程编号始终为1)
内核会在过去曾使用过init的几个地方查找它,它的正确位置(对Linux系统来说)是/sbin/init。如果内核找不到init,它就会试着运行/bin/sh,如果运行失败,系统的启动也会失败。
Linux系统中的init进程(pid=1)是除了idle进程(pid=0,也就是init_task)之外另一个比较特殊的进程,它是Linux内核开始建立起进程概念时第一个通过kernel_thread产生的进程,其开始在内核态执行,然后通过一个系统调用,开始执行用户空间的/sbin/init程序,期间Linux内核也经历了从内核态到用户态的特权级转变,/sbin/init极有可能产生出了shell,然后所有的用户进程都有该进程派生出来
0号进程是系统所有进程的先祖, 它的进程描述符init_task是内核静态创建的, 而它在进行初始化的时候, 通过kernel_thread的方式创建了两个内核线程,分别是kernel_init和kthreadd,其中kernel_init进程号为1
start_kernel在其最后一个函数rest_init的调用中,会通过kernel_thread来生成一个内核进程,后者则会在新进程环境下调 用kernel_init函数,kernel_init一个让人感兴趣的地方在于它会调用run_init_process来执行根文件系统下的 /sbin/init等程序:
简单的看一下init进程的启动过程
0号进程创建1号进程的方式如下
kernel_thread(kernel_init,NULL, CLONE_FS);
· 1
我们发现1号进程的执行函数就是kernel_init,这个函数被定义init/main.c中,如下所示
kernel_init函数将完成设备驱动程序的初始化,并调用init_post函数启动用户空间的init进程。
由0号进程创建1号进程(内核态),1号内核线程负责执行内核的部分初始化工作及进行系统配置,并创建若干个用于高速缓存和虚拟主存管理的内核线程。
随后,1号进程调用do_execve运行可执行程序init,并演变成用户态1号进程,即init进程。
init进程是linux内核启动的第一个用户级进程。init有许多很重要的任务,比如像启动getty(用于用户登录)、实现运行级别、以及处理孤立进程。
它按照配置文件/etc/initab的要求,完成系统启动工作,创建编号为1号、2号…的若干终端注册进程getty。
每个getty进程设置其进程组标识号,并监视配置到系统终端的接口线路。当检测到来自终端的连接信号时,getty进程将通过函数do_execve()执行注册程序login,此时用户就可输入注册名和密码进入登录过程,如果成功,由login程序再通过函数execv()执行shell,该shell进程接收getty进程的pid,取代原来的getty进程。再由shell直接或间接地产生其他进程。
上述过程可描述为:0号进程->1号内核进程->1号用户进程(init进程)->getty进程->shell进程
注意,上述过程描述中提到:1号内核进程调用执行init函数并演变成1号用户态进程(init进程),这里前者是init是函数,后者是进程。两者容易混淆,区别如下:
· kernel_init函数在内核态运行,是内核代码
· init进程是内核启动并运行的第一个用户进程,运行在用户态下。
· 一号内核进程调用execve()从文件/etc/inittab中加载可执行程序init并执行,这个过程并没有使用调用do_fork(),因此两个进程都是1号进程。
当内核启动了自己之后(已被装入内存、已经开始运行、已经初始化了所有的设备驱动程序和数据结构等等),通过启动用户级程序init来完成引导进程的内核部分。因此,init总是第一个进程(它的进程号总是1)。
当init开始运行,它通过执行一些管理任务来结束引导进程,例如检查文件系统、清理/tmp、启动各种服务以及为每个终端和虚拟控制台启动getty,在这些地方用户将登录系统。
在系统完全起来之后,init为每个用户已退出的终端重启getty(这样下一个用户就可以登录)。init同样也收集孤立的进程:当一个进程启动了一个子进程并且在子进程之前终止了,这个子进程立刻成为init的子进程。
二、init运行级别
运行级就是操作系统当前正在运行的功能级别。这个级别从1到6,具有不同的功能。
不同的运行级定义如下:(可以参考Red Hat Linux里面的/etc/inittab)
#0 - 关机(千万不能把initdefault设置为0)
#1 - 单用户模式
#2 - 多用户,没有 NFS
#3 - 完全多用户模式(标准的运行级)
#4 - 没有用到
#5 - X11 (xwindow)
#6 - 重新启动(千万不要把initdefault设置为6)
关于init的shell命令 init 1~6 对应上边的功能
例如:init 0 关机 init 6 重启
这些级别在/etc/inittab 文件里指定。这个文件是init 程序寻找的主要文件,最先运行的服务是放在/etc/rc.d目录下的文件。在大多数的Linux 发行版本中,启动脚本都是位于 /etc/rc.d/init.d中的。这些脚本被用ln命令连接到 /etc/rc.d/rcn.d目录。(这里的n就是运行级0-6) ,每个不同级别的目录都链接了Init.d中的命令的一部分。
Kthreadd
* kthreadd进程由idle通过kernel_thread创建,并始终运行在内核空间,负责所有内核线程的调度和管理
kthreadd (pid = 2, ppid = 0)
它的任务就是管理和调度其他内核线程kernel_thread,会循环执行一个kthread的函数,该函数的作用就是运行kthread_create_list全局链表中维护的kthread,当我们调用kernel_thread创建的内核线程会被加入到此链表中,因此所有的内核线程都是直接或者间接的以kthreadd为父进程
这种内核线程只有一个,它的作用是管理调度其它的内核线程。这个线程不能关闭。它在内核初始化的时候被创建,会循环运行一个叫做kthreadd的函数,该函数的作用是运行kthread_create_list全局链表中维护的kthread。其他任务或代码想创建内核线程时需要调用kthread_create(或kthread_create_on_node)创建一个kthread,该kthread会被加入到kthread_create_list链表中,同时kthread_create会weakup kthreadd_task(即kthreadd)(增链表)。kthreadd再执行kthread时会调用老的接口——kernel_thread运行一个名叫“kthread”的内核线程去运行创建的kthread,被执行过的kthread会从kthread_create_list链表中删除(减链表),并且kthreadd会不断调用scheduler 让出CPU。kthreadd创建的kthread执行完后,会调到kthread_create()执行,之后再执行最初原任务或代码。
注:所有的内核线程在大部分时间里都处于阻塞状态(TASK_INTERRUPTIBLE)只有在系统满足进程需要的某种资源的情况下才会运行
我们在内核中通过kernel_create或者其他方式创建一个内核线程,然后kthreadd内核线程被唤醒, 来执行内核线程创建的真正工作,新的线程将执行kthread函数,完成创建工作,创建完毕后让出CPU,因此新的内核线程不会立刻运行.需要手工 wake up,被唤醒后将执行自己的真正工作函数
· 任何一个内核线程入口都是 kthread()
· 通过 kthread_create()创建的内核线程不会立刻运行.需要手工 wake up.
Migration 进程迁移
什么是进程迁移?
进程迁移就是将一个进程从当前位置移动到指定的处理器上。它的基本思想是在进程执行过程中移动它,使得它在另一个计算机上继续存取它的所有资源并继续运行,而且不必知道运行进程或任何与其它相互作用的进程的知识就可以启动进程迁移操作,这意味着迁移是透明的。
进程迁移的好处?
进程迁移是支持负载平衡和高容错性的一种非常有效的手段。对一系列的负载平衡策略的研究表明进程迁移是实现负载平衡的基础,进程迁移在很多方面具有适用性:
· 动态负载平衡:将进程迁移到负载轻或空闲的节点上,充分利用可用资源,通过减少节点间负载的差异来全面提高性能。
容错性和高可用性:某节点出现故障时,通过将进程迁移到其它节点继续恢复运行,这将极大的提高系统的可靠性和可用性。在某些关键性应用中,这一点尤为重要。
并行文件IO:将进程迁移到文件服务器上进行IO,而不是通过传统的从文件服务器通过网络将数据传输给进程。对于那些需向文件服务器请求大量数据的进程,这将有效的减少了通讯量,极大的提高效率。
· 充分利用特殊资源:进程可以通过迁移来利用某节点上独特的硬件或软件能力。
· 内存导引(Memory Ushering)机制:当一个节点耗尽它的主存时,Memory Ushering机制将允许进程迁移到其它拥有空闲内存的节点,而不是让该节点频繁地进行分页或和外存进行交换。这种方式适合于负载较为均衡,但内存使用存在差异或内存物理配置存在差异的系统。
这种内核线程共有32个,从migration/0到migration/31,每个处理器核对应一个migration内核线程,
主要作用是作为相应CPU核的迁移进程,用来执行进程迁移操作,让每个CPU调度的任务队列均衡,内核中的函数是migration_thread()。
属于2.6内核的负载平衡系统,该进程在系统启动时自动加载(每个 cpu一个),并将自己设为 SCHED_FIFO的实时进程,然后检查 runqueue::migration_queue中是否有请求等待处理,如果没有,就在 TASK_INTERRUPTIBLE中休眠,直至被唤醒后再次检查。migration_thread()仅仅是一个 CPU 绑定以及 CPU 电源管理等功能的一个接口。这个线程是调度系统的重要组成部分。
Ksoftirqd 软中断守护进程
[ksoftirqd/0] 内核调度/管理第0个CPU软中断的守护进程
[ksoftirqd/1] 内核调度/管理第1个CPU软中断的守护进程
每个处理器都有一组辅助处理器软中断(和tasklet)的内核线程ksoftirq。当内核中出现大量软中断的时候,这些内核进程就会辅助处理它们。
引入ksoftirq内核线程的原因:
这个线程正是用来执行软中断的(准确的说应该是执行过多的软中断)。我们知道按照优先级来说,中断>软中断>用户进行,也就是说中断可以打断软中断,而软中断又可以打断用户进程。
对于软中断,内核会在几个特殊的时机执行(注意执行和调度的区别,调度软中断只是对软中断打上待执行的标记,并没有真正执行),而在中断处理程序返回时处理是最常见的。软中断的触发频率有时可能会很高(例如进行大流量网络通信期间)。更不利的是,软中断的执行函数有时还会调度自身(软中断嵌套),所以如果软中断本身出现的频率较高,再加上他们又有将自己重新设置为可执行状态的能力,那么就会导致用户空间的进程无法获得足够的处理时间,因而处于饥饿状态。 单纯的对重新触发的软中断采取不立即处理的策略,也无法让人接受。
最初的解决方案:
1)只要还有被触发并等待处理的软中断,本次执行就要负责处理,重新触发的软中断也在本次执行返回前被处理。这样做可以保证对内核的软中断采取即时处理的方式,关键在于,对重新触发的软中断也会立即处理。当负载很高的时候,此时若有大量被触发的软中断,而它们本身又可能会重复触发。系统可能会一直处理软中断根本不能完成其他任务。
2)不处理重新触发的软中断。在从中断返回的时候,内核和平常一样,也会检查所有挂起的软中断并处理他们。但是,任何自行重新触发的软中断不会马上处理,它们被放到下一个软中断执行时机去处理。而这个时机通常也就是下一次中断返回的时候。可是,在比较空闲的系统中,立即处理软中断才是比较好的做法。尽管它能保证用户空间不处于饥饿状态,但它却让软中断忍受饥饿的痛苦,而根本没有好好利用闲置的系统资源。
改进:引入ksoftirq内核线程
最终在内核中实现的方案是不会立即处理处理重新触发的软中断。而作为改进,当大量软中断出现的时候,内核会唤醒一组内核线程来处理这些负载。这些线程在最低的优先级上运行(nice值是19),这能避免它们跟其他重要的任务抢夺资源。但它们最终肯定会被执行,所以这个折中方案能够保证在软中断负担很中的时候用户程序不会因为得不到处理时间处于饥饿状态。相应的,也能保证”过量“的软中断终究会得到处理。
每个处理器都有一个这样的线程。所有线程的名字都叫做ksoftirq/n,区别在于n,它对应的是处理器的编号。在一个双CPU的机器上就有两个这样的线程,分别叫做ksoftirqd/0和ksoftirqd/1。为了保证只要有空闲的处理器,它们就会处理软中断,所以给每个处理器都分配一个这样的线程
Watchdogd linux的看门狗
这种内核线程共有32个,从watchdog/0到watchdog/31, 每个处理器核对应一个watchdog 内核线程,
watchdog用于监视系统的运行,在系统出现故障时自动重新启动系统,包括一个内核 watchdog module和一个用户空间的 watchdog程序。
在Linux 内核下, watchdog的基本工作原理是:
内核 watchdog模块通过 /dev/watchdog这个字符设备与用户空间通信。用户空间程序一旦打开/dev/watchdog设备("开门放狗"),就会导致在内核中启动一个1分钟的定时器,此后,用户空间程序需要保证在 1分钟之内向这个设备写入数据("定期喂狗"),每次写操作会导致重新设定定时器。如果用户空间程序在1分钟之内没有写操作,定时器到期会导致一次系统 reboot 操作("人被狗咬")。
通过这种机制,我们可以保证系统核心进程大部分时间都处于运行状态,即使特定情形下进程崩溃,因无法正常定时“喂狗”,Linux系统在看门狗作用下重新启动(reboot),核心进程又运行起来了。多用于嵌入式系统。
Events
这种内核线程共有32个,从events/0到events/31, 每个处理器核对应一个 events内核线程来处理内核事件,很多软硬件事件(比如断电,文件变更)被转换为events,并分发给对相应事件感兴趣的线程进行响应
在Linux内核中工这个线程就是工作队列(workqueue)用来执行队列中的工作的。
接下来看看工作队列 和 events线程的关系, 先了解下工作队列
1. 什么是workqueue?
Workqueue也是linux下半部(包括软中断、tasklet、工作队列)实现的一种方式。Linux中的Workqueue机制就是为了简化内核线程的创建。通过调用workqueue的接口就能创建内核线程。并且可以根据当前系统CPU的个数创建线程的数量,使得线程处理的事务能够并行化。
Workqueue是内核中实现简单而有效的机制,他显然简化了内核daemon的创建,方便了用户的编程。
2. Workqueue机制的实现
当用户调用workqueue的初始化接口create_workqueue或者create_singlethread_workqueue对workqueue队列进行初始化时,内核就开始为用户分配一个workqueue对象,并且将其链到一个全局的workqueue队列中。然后Linux根据当前CPU的情况,为workqueue对象分配与CPU个数相同的cpu_workqueue_struct对象,每个cpu_workqueue_struct对象都会存在一条任务队列。紧接着,Linux为每个cpu_workqueue_struct对象分配一个内核thread,即内核daemon去处理每个队列中的任务。至此,用户调用初始化接口将workqueue初始化完毕,返回workqueue的指针。
在初始化workqueue过程中,内核需要初始化内核线程,注册的内核线程工作比较简单,就是不断的扫描对应cpu_workqueue_struct中的任务队列,从中获取一个有效任务,然后执行该任务。所以如果任务队列为空,那么内核daemon就在cpu_workqueue_struct中的等待队列上睡眠,直到有人唤醒daemon去处理任务队列。
Workqueue初始化完毕之后,将任务运行的上下文环境构建起来了,但是具体还没有可执行的任务,所以,需要定义具体的work_struct对象。然后将work_struct加入到任务队列中,Linux会唤醒daemon去处理任务。
上述描述的workqueue内核实现原理可以描述如下:
在Workqueue机制中,提供了一个系统默认的workqueue队列——keventd_wq,这个队列是Linux系统在初始化的时候就创建的。用户可以直接初始化一个work_struct对象,然后在该队列中进行调度,使用更加方便。
我们看到的events/0,events/1这些内核线程就是这个默认工作队列在每个cpu上创建的用来执行任务(work)的kthread。