Linux内核线程之深入浅出【转】 - 张昺华

发布时间:2017-7-9 7:18:10编辑:www.fx114.net 分享查询网我要评论
本篇文章主要介绍了"Linux内核线程之深入浅出【转】 - 张昺华",主要涉及到Linux内核线程之深入浅出【转】 - 张昺华方面的内容,对于Linux内核线程之深入浅出【转】 - 张昺华感兴趣的同学可以参考一下。

根据操作系统内核是否对线程可感知,可以把线程分为内核线程和用户线程。

内核线程建立和销毁都是由操作系统负责、通过系统调用完成的,操作系统在调度时,参考各进程内的线程运行情况做出调度决定,如果一个进程中没有就绪态的线程,那么这个进程也不会被调度占用CPU。

和内核线程相对应的是用户线程,用户线程指不需要内核支持而在用户程序中实现的线程,其不依赖于操作系统核心,用户进程利用线程库提供创建、同步、调度和管理线程的函数来控制用户线程。用户线程多见于一些历史悠久的操作系统,例如Unix操作系统,不需要用户态/核心态切换,速度快,操作系统内核不知道多线程的存在,因此一个线程阻塞将使得整个进程(包括它的所有线程)阻塞。由于这里的处理器时间片分配是以进程为基本单位,所以每个线程执行的时间相对减少为了在操作系统中加入线程支持,采用了在用户空间增加运行库来实现线程,这些运行库被称为“线程包”,用户线程是不能被操作系统所感知的。

引入用户线程,具体而言,有以下四个方面的优势:

(1)       可以在不支持线程的操作系统中实现。

(2)       创建和销毁线程、线程切换代价等线程管理的代价比内核线程少得多。

(3)       允许每个进程定制自己的调度算法,线程管理比较灵活。

(4)       线程能够利用的表空间和堆栈空间比内核级线程多。

用户线程的缺点主要有以下两点:

(1)       同一进程中只能同时有一个线程在运行,如果有一个线程使用了系统调用而阻塞,那么整个进程都会被挂起。

(2)       页面失效也会产生类似的问题。

内核线程的优缺点刚好跟用户线程相反。实际上,操作系统可以使用混合的方式来实现线程。

1      线程和进程的差别

在现代操作系统中,进程支持多线程。进程是资源管理及分配的最小单元;而线程是程序执行的最小单元。一个进程的组成实体可以分为两大部分:线程集和资源集。进程中的线程是动态的对象,代表了进程指令的执行过程。资源,包括地址空间、打开的文件、用户信息等等,由进程内的线程共享。线程有自己的私有数据:程序计数器,栈空间以及寄存器。

 

现实中有很多需要并发处理的任务,如数据库的服务器端、网络服务器、大容量计算等。传统的UNIX进程是单线程的,单线程意味着程序必须是顺序执行,不能并发,即在一个时刻只能运行在一个处理器上,因此不能充分利用多处理器框架的计算机。

 

如果采用多进程的方法,则有如下问题:

?      fork一个子进程的消耗是很大的,fork是一个昂贵的系统调用。

?      各个进程拥有自己独立的地址空间,进程间的协作需要复杂的IPC技术,如消息传递和共享内存等。

 

线程推广了进程的概念,使一个进程可以包含多个活动(或者说执行序列等等)。多线程的优点和缺点实际上是对立统一的。使用线程的优点在于:

?      改进程序的实时响应能力;

?      更有效的使用多处理器,真正的并行(parallelism)

?      改进程序结构,具备多个控制流;

?      通讯方便,由于共享进程的代码和全局数据;

?      减少对系统资源的使用。对属于同一个进程的线程之间进行调度切换时不需要调用系统调用,因此将减少额外的消耗,往往一个进程可以启动上千个线程也没有什么问题。 

缺点在于:

由于各线程共享进程的地址空间,因此可能会导致竞争,因此对某一块有多个线程要访问的数据需要一些同步技术。

 

2      线程的分类

2.1     内核线程

Linux内核可以看作一个服务进程(管理软硬件资源,响应用户进程的种种合理以及不合理的请求)。内核需要多个执行流并行,为了防止可能的阻塞,多线程化是必要的。内核线程就是内核的分身,一个分身可以处理一件特定事情。linux内核使用内核线程来将内核分成几个功能模块,像kswapd、kflushd等,这在处理异步事件如异步IO时特别有用。内核线程的使用是廉价的,唯一使用的资源就是内核栈和上下文切换时保存寄存器的空间。支持多线程的内核叫做多线程内核(Multi-Threads kernel )。内核线程的调度由内核负责,一个内核线程处于阻塞状态时不影响其他的内核线程,因为其是调度的基本单位。这与用户线程是不一样的。

 

2.2     轻量级进程

轻量级进程(LWP)是一种由内核支持的用户线程。它是基于内核线程的高级抽象,因此只有先支持内核线程,才能有LWP。每一个进程有一个或多个LWPs,每个LWP由一个内核线程支持。这种模型实际上就是恐龙书上所提到的一对一线程模型。在这种实现的操作系统中,LWP就是用户线程。

 

由于每个LWP都与一个特定的内核线程关联,因此每个LWP都是一个独立的线程调度单元。即使有一个LWP在系统调用中阻塞,也不会影响整个进程的执行。

 

轻量级进程具有局限性。首先,大多数LWP的操作,如建立、析构以及同步,都需要进行系统调用。系统调用的代价相对较高:需要在user modekernel mode中切换。其次,每个LWP都需要有一个内核线程支持,因此LWP要消耗内核资源(内核线程的栈空间)。因此一个系统不能支持大量的LWP。

注:

LWP的术语是借自于SVR4/MP和Solaris 2.x。将之称为轻量级进程的原因可能是:在内核线程的支持下,LWP是独立的调度单元,就像普通的进程一样。所以LWP的最大特点还是每个LWP都有一个内核线程支持。

 

2.3     用户线程

LWP虽然本质上属于用户线程,LWP线程库是建立在内核之上的,LWP的许多操作都要进行系统调用,因此效率不高。而这里的用户线程指的是指不需要内核支持而完全建立在用户空间的线程库,用户线程的建立、同步、销毁及调度完全在用户空间完成,不需要内核的参与帮助。因此这种线程的操作是极其快速的且低消耗的。

 

上图是最初的一个多对一用户线程模型,从中可以看出,进程中包含线程,用户线程在用户空间中实现,内核并没有直接对用户线程进程调度,内核的调度对象和传统进程一样,还是进程本身,内核并不知道用户线程的存在。

 

由于Linux内核没有轻量级进程(线程)的概念,因此不能独立的对用户线程进行调度,而是由一个线程运行库来组织线程的调度,其主要工作在于在各个线程的栈之间调度。如果一个进程中的某一个线程调用了一个阻塞的系统调用,整个进程就会被调度程序切换为等待状态,其他线程得不到运行的机会。因此UNIX使用了异步I/O机制。这种机制主要的缺点在于在一个进程中的多个线程的调度中无法发挥多处理器的优势(如上述的阻塞情况)。

 

3      初识内核线程

内核线程(thread)或叫守护进程(daemon),在操作系统中占据相当大的比例,当Linux操作系统启动以后,尤其是X window也启动以后,你可以用”ps -ef”命令查看系统中的进程,这时会发现很多以”d”结尾的进程名,确切说名称显示里面加 "[]"的,这些进程就是内核线程。系统的启动是从硬件->内核->用户态进程的,pid的分配是一个往前的循环的过程,所以随系统启动的内核线程的pid往往很小。

 

?      PID TTY STAT TIME COMMAND

?      1 ? S 0:01 init [3]

?      3 ? SN 0:00 [ksoftirqd/0]

?      5 ? SN 0:00 [ksoftirqd/1]

?      6 ? S< 0:00 [events/0]

?      7 ? S< 0:00 [events/1]

?      8 ? S< 0:00 [khelper]

?      9 ? S< 0:00 [kblockd/0]

?      10 ? S< 0:00 [kblockd/1]

?      11 ? S 0:00 [khubd]

?      35 ? S 0:42 [pdflush]

?      36 ? S 0:02 [pdflush]

?      38 ? S< 0:00 [aio/0]

?      39 ? S< 0:00 [aio/1]

?      37 ? S 0:19 [kswapd0]

?      112 ? S 0:00 [kseriod]

?      177 ? S 0:00 [scsi_eh_0]

?      178 ? S 0:00 [ahc_dv_0]

?      188 ? S 0:00 [scsi_eh_1]

?      189 ? S 0:00 [ahc_dv_1]

?      196 ? S 2:31 [kjournald]

?      1277 ? S 0:00 [kjournald]

?      1745 ? Ss 0:02 syslogd -m 0

?      1749 ? Ss 0:00 klogd -x

?      1958 ? Ss 0:13 /usr/sbin/sshd

?      2060 ? Ss 0:00 crond

?      2135 tty2 Ss+ 0:00 /sbin/mingetty tty2

?      2136 tty3 Ss+ 0:00 /sbin/mingetty tty3

?      2137 tty4 Ss+ 0:00 /sbin/mingetty tty4

?      2138 tty5 Ss+ 0:00 /sbin/mingetty tty5

?      2139 tty6 Ss+ 0:00 /sbin/mingetty tty6

?      23564 ? S 0:00 bash

?      25605 ? Ss 0:00 sshd: peter [priv]

?      25607 ? S 0:00 sshd: peter@pts/2

 

3.1     内核线程

?      events 处理内核事件 很多软硬件事件(比如断电,文件变更)被转换为events,并分发给对相应事件感兴趣的线程进行响应

?      ksoftirqd 处理软中断 硬件中断处理往往需要关中断,而这个时间不能太长,否则会丢失新的中断。所以中断处理的很大一部分工作移出,转给任劳任怨的ksoftirqd在中断之外进行处理。比如一个网络包,从网卡里面取出这个过程可能需要关中断,但是TCP/IP协议处理就不必关中断了

?      kblockd 管理磁盘块读写

?      kjournald Ext3文件系统的日志管理 通常每个 _已mount_ 的Ext3分区会有一个 kjournald看管,各分区的日志是独立

?      pdflush dirty内存页面的回写 太多dirty的页面意味着风险,比如故障时候的内容丢失,以及对突发的大量物理内存请求的响应(大量回写会导致糟糕的响应时间)

?      kswapd 内存回收 确保系统空闲物理内存的数量在一个合适的范围

?      aio 代替用户进程管理io 用以支持用户态的AIO

3.2     用户进程

?      crond 执行定时任务

?      init 为内核创建的第一个线程。引导用户空间服务,管理孤儿线程,以及运行级别的转换

?      mingetty 等待用户从tty登录

?      bash shell进程,一个命令行形式的系统接口;接受用户的命令,并进行解释、执

?      sshd ssh登录、文件传输、命令执行 等操作的服务进程

?      klogd 从内核信息缓冲区获取打印信息。内核在发现异常的时候,往往会输出一些消息给用户,这个对于故障处理很有用

?      syslogd 系统日志进程

?      udevd 支持用户态设备操作 (userspace device)

4      内核线程与用户进程的关系

内核线程也可以叫内核任务,例如,磁盘高速缓存的刷新,网络连接的维护,页面的换入换出等等。在Linux中,内核线程与普通进程有一些本质的区别,从以下几个方面可以看出二者之间的差异:

?      内核线程能够访问内核中数据,调用内核函数,而普通进程只有通过系统调用才能执行内核中的函数;

?      内核线程只运行在内核态,而普通进程既可以运行在用户态,也可以运行在内核态;

?      因为内核线程指只运行在内核态,因此,它只能使用大于PAGE_OFFSET3G)的地址空间。另一方面,不管在用户态还是内核态,普通进程可以使用4GB的地址空间。

 

5      内核线程的创建

内核线程是由kernel_thread(  )函数在内核态下创建的:

linux+v2.6.19/arch/arm/kernel/process.c

 460 pid_t kernel_thread(int (*fn)(void *), void *arg, unsigned long flags)

 461{

 462        struct pt_regs regs;

 463

 464        memset(&regs, 0, sizeof(regs));

 465

 466        regs.ARM_r1 = (unsigned long)arg;

 467        regs.ARM_r2 = (unsigned long)fn;

 468        regs.ARM_r3 = (unsigned long)do_exit;

 469        regs.ARM_pc = (unsigned long)kernel_thread_helper;

 470        regs.ARM_cpsr = SVC_MODE;

 471

 472        return do_fork(flags|CLONE_VM|CLONE_UNTRACED, 0, &regs, 0, NULLNULL);

 473}

 

Regs:内核栈中的一个地址,通过这个地址,可以找到内核栈CPU寄存器的初始化值。

线程的创建本质上就是建立了一个task_struct进程结构,这样内核调度时就会根据此结构的Regs信息运行。

 

建立新的内核线程时默认采用了如下标志:

CLONE_VM:置起此标志在进程间共享地址空间。因此内核线程和调用的进程(current)具备相同的进程空间,因为调用者运行在进程的内核态,所以进程在内核态时共享内核空间。

CLONE_UNTRACED:保证即使父进程正在被调试,内核线程也不会被调试。

其他调用时可用的标志如下: CLONE_FS置起此标志在进程间共享文件系统信息 CLONE_FILES置起此标志在进程间共享打开的文件CLONE_SIGHAND置起此标志在进程间共享信号处理程序

 

建立线程完毕后将其状态设置为TASK_RUNNING,然后放入运行队列,等待调度。kernel_thread的返回值同sys_fork,正数为新建线程的pid,否则为错误代码。kernel_thread.未返回时,新建线程已经退出,则返回值为0

 

新线程中执行fn函数,arg为传递给新建线程fn的参数,应确保fn未退出时arg可用。

 

如果父进程未得到子进程退出信息时已经退出,则内核线程将变为孤儿进程。新建内核线程的父进程是模块的加载者,或者确切的说是调用kernel_thread的进程。

 

kernel_thread_helper是一个汇编函数,代码如下:

 441/*

 442 * Shuffle the argument into the correct register before calling the

 443 * thread function.  r1 is the thread argument, r2 is the pointer to

 444 * the thread function, and r3 points to the exit function.

 445 */

 446extern void kernel_thread_helper(void);

 447asm(    ".section .text/n"

 448"       .align/n"

 449"       .type   kernel_thread_helper, #function/n"

 450"kernel_thread_helper:/n"

 451"       mov     r0, r1/n"

 452"       mov     lr, r3/n"

 453"       mov     pc, r2/n"

 454"       .size   kernel_thread_helper, . - kernel_thread_helper/n"

 455"       .previous");

Pc执行r2即待创建的线程执行体,arg参数r1赋值给r0,传递给r2的执行线程,lr为ARM的链接寄存器,当从r2返回时将赋给pc,即执行r3指向的do_exit,进行线程的清除工作。

所以内核线程是通过fn函数开始的。当结束时,执行系统调用do_exit。本质上相当于运行exit(fn(arg))。

 

6      如何在驱动中使用内核线程

/* * file mythread.c */

#include <linux/kernel.h>

#include <linux/module.h>

#include <linux/init.h>

#include <linux/param.h>

#include <linux/jiffies.h>

#include <asm/system.h>

#include <asm/processor.h>

#include <asm/signal.h>

static pid_t thread_id;

static DECLARE_COMPLETION(exit_completion);

static atomic_t time_to_quit = ATOMIC_INIT(0);

 

int my_fuction(void *arg) {     

int ret;

daemonize("demo-thread");        

allow_signal(SIGKILL);

complete (&exit_completion);

      while(!signal_pending(current))        

     {                

                printk("jiffies is %lu/n", jiffies);

                set_current_stat(TASK_INTERRUPTIBLE);                                   

               schedule_timeout(10 * HZ);

if (atomic_read(&kthread->terminate))

{ /* we received a request to terminate ourself */ break; }

     }

/*

for(;;)

{

Ret = wait_event_interruptible(wq, condition); //可采用任何同步措施

}*/

  complete_and_exit(&exit_completion, 1); 

  return 0;

}

static int __init init(void)

{        

thread_id = kernel_thread(my_fuction, NULL, CLONE_FS | CLONE_FILES);

wait_for_completion(&exit_completion);

return 0;

}

static void __exit finish(void)

{     

atomic_inc(&time_to_quit); 

        kill_proc(thread_id, SIGKILL, 1); 

        wait_for_completion(&exit_completion);

         printk("Goodbye/n");

}

module_init(init);

module_exit(finish);

MODULE_LICENSE("GPL");


上一篇:_DataStructure_C_Impl:求图G中从顶点u到顶点v的一条简单路径
下一篇:第四十期百度技术沙龙笔记整理

相关文章

相关评论

本站评论功能暂时取消,后续此功能例行通知。

一、不得利用本站危害国家安全、泄露国家秘密,不得侵犯国家社会集体的和公民的合法权益,不得利用本站制作、复制和传播不法有害信息!

二、互相尊重,对自己的言论和行为负责。

好贷网好贷款