Linux-4.5
X86&arm
gatieme
LinuxDeviceDrivers
Linux进程管理与调度-之-进程的描述
Linux内核可以看作一个服务进程(管理软硬件资源,响应用户进程的种种合理以及不合理的恳求)。
内核须要多个执行流并行,为了避免可能的阻塞,支持多线程是必要的。
内核线程就是内核的分身,一个分身可以处理一件特定事情。内核线程的调度由内核负责,一个内核线程处于阻塞状态时不影响其他的内核线程,由于其是调度的基本单位。
这与用户线程是不一样的。由于内核线程只运行在内核态
因而,它只能使用小于PAGE_OFFSET(传统的x86_32上是3G)的地址空间。
内核线程概述
内核线程是直接由内核本身启动的进程。内核线程实际上是将内核函数委托给独立的进程,它与内核中的其他进程”并行”执行。内核线程常常被称之为内核守护进程。
她们执行下述任务
内核线程主要有两种类型
线程启动后仍然等待,直到内核恳求线程执行某一特定操作。
线程启动后按周期性间隔运行,测量特定资源的使用,在药量超出或高于预置的限制时采取行动。
内核线程由内核自身生成,其特征在于
它们在CPU的管态执行,而不是用户态。
它们只可以访问虚拟地址空间的内核部份(低于TASK_SIZE的所有地址),但不能访问用户空间
内核线程的进程描述符task_struct
task_struct进程描述符中包含两个跟进程地址空间相关的数组mm,active_mm,
struct task_struct
{
// ...
struct mm_struct *mm;
struct mm_struct *avtive_mm;
//... };
大多数计算机上系统的全部虚拟地址空间分为两个部份:供用户态程序访问的虚拟地址空间和供内核访问的内核空间。每每内核执行上下文切换时,虚拟地址空间的用户层部份就会切换,以便当前运行的进程匹配,而内核空间不会放生切换。
对于普通用户进程来说,mm指向虚拟地址空间的用户空间部份,而对于内核线程,mm为NULL。
那位优化提供了一些余地,可遵照所谓的惰性TLB处理(lazyTLBhanding)。active_mm主要用于优化,因为内核线程不与任何特定的用户层进程相关,内核并不须要倒换虚拟地址空间的用户层部份,保留旧设置即可。因为内核线程之前可能是任何用户层进程在执行,故用户空间部份的内容本质上是随机的,内核线程决不能更改其内容,故将mm设置为NULL,同时若果切换出去的是用户进程,内核将原先进程的mm储存在新内核线程的active_mm中,由于个别时侯内核必须晓得用户空间当前包含了哪些。
为何没有mm表针的进程称为惰性TLB进程?
如果内核线程然后运行的进程与之前是同一个,在这些情况下,内核并不须要更改用户空间地址表。地址转换后备缓冲器(即TLB)中的信息依然有效。只有在内核线程以后,执行的进程是与此前不同的用户层进程时,才须要切换(并对应消除TLB数据)。
内核线程和普通的进程间的区别在于内核线程没有独立的地址空间,mm表针被设置为NULL;它只在内核空间运行,从来不切换到用户空间去;而且和普通进程一样,可以被调度,也可以被占领。
内核线程的创建创建内核线程插口的演化
内核线程可以通过两种方法实现:
2号进程kthreadd的诞生
初期的kernel_create和daemonize插口
在初期的内核中,提供了kernel_create和daemonize插口,并且这些机制操作复杂并且将所有的任务交给内核去完成。
然而这些机制低效并且冗长,将所有的操作塞给内核,我们创建内核线程的本意不原本就是为了内核分担工作,降低内核的开支的么
Workqueue机制
因而在linux-2.6之后,提供了愈加便捷的插口kthead_create和kthread_run,同时将内核线程的创建操作延迟,交给一个工作队列workqueue,参见,
Linux中的workqueue机制就是为了简化内核线程的创建。通过kthread_create并不真正创建内核线程,而是将创建工作creatework插入到工作队列中,此后调用workqueue的插口才能创建内核线程。而且可以按照当前系统CPU的个数创建线程的数目,致使线程处理的事务才能并行化。workqueue是内核中实现简单而有效的机制,他似乎简化了内核daemon的创建,便捷了用户的编程.
工作队列(workqueue)是另外一种将工作推后执行的方式.工作队列可以把工作推后,交由一个内核线程去执行,也就是说,这个下半部份可以在进程上下文中执行。最重要的就是工作队列容许被重新调度甚至是睡眠。
具体的信息,请参见
Linuxworkqueue工作原理
2号进程kthreadd
然而这些技巧仍旧看上去不够优美,我们何不把这些创建内核线程的工作交给一个特殊的内核线程来做呢?
于是linux-2.6.22引入了kthreadd进程,并随即演化为2号进程,它在系统初始化时同1号进程一起被创建(其实肯定是通过kernel_thread),,并随即演化为创建内核线程的真正建造师,参见kthreadd和,它会循环的是查询工作数组中是否有须要被创建的内核线程,而我们的通过kthread_create执行的操作,只是在内核线程任务队列kthread_create_list中降低了一个create任务,之后会唤起kthreadd进程来执行真正的创建操作
内核线程会出现在系统进程列表中,并且在ps的输出中进程名command由方括弧包围,便于与普通进程分辨。
如右图所示,我们可以看见系统中,所有内核线程都用[]标示,并且这种进程父进程id均是2,而2号进程kthreadd的父进程是0号进程
使用ps-eopid,ppid,command
kernel_thread
kernel_thread是最基础的创建内核线程的插口,它通过将一个函数直接传递给内核来创建一个进程,创建的进程运行在内核空间,但是与其他进程线程共享内核虚拟地址空间
kernel_thread的实现经历过好多改革
初期的kernel_thread执行更底层的操作,直接创建了task_struct并进行初始化,
引入了kthread_create和kthreadd2号进程后,kernel_thread的实现也由统一的_do_fork(或则初期的do_fork)托管实现
初期实现
初期的内核中,kernel_thread并不是使用统一的do_fork或则_do_fork这一封装好的插口实现的,而是使用更底层的细节
参见
我们可以看见它内部调用了愈加底层的arch_kernel_thread创建了一个线程
arch_kernel_thread
其具体实现请参见
;i=arch_kernel_thread
然而这些方法创建的线程并不适宜运行,因而内核提供了daemonize函数,其申明在include/linux/sched.h中
// http://lxr.free-electrons.com/source/include/linux/sched.h?v=2.4.37#L800
extern void daemonize(void);
定义在kernel/sched.c
主要执行如下操作
该函数释放其父进程的所有资源,不然这种资源会仍然锁定直至线程结束。
阻塞讯号的接收
将init用作守护进程的父进程
我们可以看见初期内核的好多地方使用了这个插口,例如
可以参见
;i=daemonize
我们将了如此多kernel_thread,并且我们并不倡导我们使用它,由于这个是底层的创建内核线程的操作插口,使用kernel_thread在内核中执行大量的操作,尽管创建的代价早已很小了,而且对于追求性能的linux内核来说还不能忍受
因而我们只能说kernel_thread是一个古老的插口,内核中的有些地方依然在使用该方式,将一个函数直接传递给内核来创建内核线程
新版本的实现
于是linux-3.x下以后,有了更好的实现,那就是
延迟内核的创建工作,将内核线程的创建工作交给一个内核线程来做,即kthreadd2号进程
然而在kthreadd还没创建之前,我们只能通过kernel_thread这些方法去创建,
同时kernel_thread的实现也改为由_do_fork(初期内核中是do_fork)来实现,参见
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); }
kthread_create
struct task_struct *kthread_create_on_node(int (*threadfn)(void *data),
void *data,
int node, const char namefmt[], ...); #define kthread_create(threadfn, data, namefmt, arg...) kthread_create_on_node(threadfn, data, NUMA_NO_NODE, namefmt, ##arg)
创建内核更常用的方式是辅助函数kthread_create,该函数创建一个新的内核线程。最初线程是停止的,须要使用wake_up_process启动它。
kthread_run
/**
* kthread_run - create and wake a thread.
* @threadfn: the function to run until signal_pending(current).
* @data: data ptr for @threadfn.
* @namefmt: printf-style name for the thread.
*
* Description: Convenient wrapper for kthread_create() followed by
* wake_up_process(). Returns the kthread or ERR_PTR(-ENOMEM).
*/
#define kthread_run(threadfn, data, namefmt, ...)
({
struct task_struct *__k
= kthread_create(threadfn, data, namefmt, ## __VA_ARGS__);
if (!IS_ERR(__k)) wake_up_process(__k); __k; })
使用kthread_run,与kthread_create不同的是,其创建新线程后立刻唤起它,其本质就是先用kthread_create创建一个内核线程,之后通过wake_up_process唤起它
内核线程的退出
线程一旦启动上去后linux软件下载linux软件工程师,会仍然运行,除非该线程主动调用do_exit函数,或则其他的进程调用kthread_stop函数,结束线程的运行。
int kthread_stop(struct task_struct *thread);
kthread_stop()通过发送讯号给线程。
假如线程函数正在处理一个特别重要的任务linux进程与线程 内核,它不会被中断的。其实假如线程函数永远不返回而且不检测讯号,它将永远都不会停止。
在执行kthread_stop的时侯,目标线程必须没有退出,否则会Oops。缘由很容易理解linux进程与线程 内核,当目标线程退出的时侯,其对应的task结构也显得无效,kthread_stop引用该无效task结构还会出错。
为了防止这些情况,须要确保线程没有退出,其方式如代码中所示:
thread_func()
{
// do your work here
// wait to exit
while(!thread_could_stop())
{
wait();
}
}
exit_code()
{
kthread_stop(_task); //发信号给task,通知其可以退出了
}
这些退出机制很温和,一切尽在thread_func()的掌控之中,线程在退出时可以从容地释放资源,而不是莫名其妙地被人“暗杀”。
【作者】张昺华
【出处】
【博客园】
【新浪博客】
【知乎】
【我的作品---旋转倒立摆】
【我的作品---自平衡手动避障车】
【新浪微博】张昺华--sky
【twitter】@sky2030_
【facebook】张昺华zhangbinghua