Linux内核线程之深入浅出
http://blog.csdn.net/sailor_8318/archive/
2008/06/04/2509320.aspx
【摘要】本文首先介绍了进程和线程的区别,接着分析了内核线程、轻量级LWP线程以及常见的用户线程的特点,同时介绍了内核线程和进程的区别。分析了创建内核线程kernel_thread函数的实现过程,介绍了一个在驱动中使用内核线程的实例。最后针对内核线程创建销毁的特点,给出了通用的内核线程操作函数API,使用该API可在自己的驱动或内核代码中方便的使用内核线程。
【关键词】线程,进程,内核线程,用户线程,LWP,kernel_thread,daemonize,kill_proc,allow_signal,SIGKILL
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 mode和kernel 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_OFFSET(
3G)的地址空间。另一方面,不管在用户态还是内核态,普通进程可以使用4GB的地址空间。
5内核线程的创建
内核线程是由kernel_thread()函数在内核态下创建的:
linux+v
2.6.19/arch/arm/kernel/process.c
460pid_tkernel_thread(int (*fn)(void *), void *arg, unsigned longflags)
464memset(®s, 0, sizeof(regs));
466regs.ARM_r1= (unsigned long)arg;
467regs.ARM_r2= (unsigned long)fn;
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是一个汇编函数,代码如下:
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.
447asm(".section .text/n"
448".align/n"
449".typekernel_thread_helper, #function/n"
450"kernel_thread_helper:/n"
451"movr0, r1/n"
452"movlr, r3/n"
453"movpc, r2/n"
454".sizekernel_thread_helper, . - kernel_thread_helper/n"
455".previous");
Pc执行r2即待创建的线程执行体,arg参数r1赋值给r0,传递给r2的执行线程,lr为ARM的链接寄存器,当从r2返回时将赋给pc,即执行r3指向的,进行线程的清除工作。
所以内核线程是通过fn函数开始的。当结束时,执行系统调用do_exit。本质上相当于运行exit(fn(arg))。
6如何在驱动中使用内核线程
/* * file mythread.c */
#include
#include
#include
#include
#include
#include
#include
#include
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");
7通用的内核线程模块
7.1内核线程相关API
本驱动实例展示如何创建及停止一个内核线程。以模块形式加载,相关接口函数export到内核中,供其他模块使用。
内核版本2.6.19,包含kthread.h和kthread.c文件。
²start_kthread:
建立一个内核线程。可在任何进程上下文中执行,down()阻塞在同步信号量semaphore上直至新建立线程启动后执行init_kthread,其调用up()通知start_kthread新线程建立成功并运行。
²stop_kthread:
清除一个内核线程。可在除待清楚进程之外的任何进程上下文中执行。设置退出标志供所建线程检测并发送SIGKILL信号给该线程,阻塞,直至其调用exit_kthread通知killer其已经安全退出。
²init_kthread:
初始化新建立的线程环境,在新建线程循环外部执行。其将调用daemonize更新父进程为init进程便于回收资源,设置待捕捉的信号以便killer可以清除该线程。同时设置新线程的名称,清除退出标志,up()通知建立者新线程成功建立。
²exit_kthread:
收到中止信号后调用,退出。up()通知stop_kthread()其已经退出。
7.2kthread.h
#ifndef _KTHREAD_H
#define _KTHREAD_H
#include
#include
#include
#include
#include
#include
#include
#include
/*新建线程的所以相关信息*/
typedef struct kthread_struct {
/* private data */
/* New Linux task structure of thread,新建线程的进程数据结构,由current获得*/
struct task_struct *thread;
/* function to be started as thread,待执行的任务*/
void (*function) (struct kthread_struct *kthread);
/* semaphore needed on start and creation of thread.新建或清除过程中用来同步建立者和清除者与所建线程*/
struct semaphore startstop_sem;
/* public data */
/* queue thread is waiting on. Gets initialized by init_kthread, can be used by thread itself.新线程用来或者外部事件的等待队列*/
wait_queue_head_t queue;
/* flag to tell thread whether to die or not. When the thread receives a signal, it must check the value of terminate and call exit_kthread and terminate if set.。killer设置的退出标识*/
atomic_tterminate;
/* additional data to pass to kernel thread。传递给新建线程的附加信息,可选*/
void *arg;
} kthread_t;
/*原型声明*/
/* start new kthread (called by creator) */
void start_kthread(void (*func)(kthread_t *), kthread_t *kthread);
/* stop a running thread (called by "killer") */
void stop_kthread(kthread_t *kthread);
/* setup thread environment (called by new thread) */
void init_kthread(kthread_t *kthread, char *name);
/* cleanup thread environment (called by thread upon receiving termination signal) */
void exit_kthread(kthread_t *kthread);
#endif
7.3kthread.c
#include
#include
#if defined(MODVERSIONS)
#include
#endif
#include
#include
#include
#include
#include
#include
#include
#include "kthread.h"
/* public functions */
/* create a new kernel thread. Called by the creator. */
void start_kthread(void (*func)(kthread_t *), kthread_t *kthread)
{
/* initialize the semaphore:we start with the semaphore locked. The new kernel thread will setup its stuff and unlock it. This control flow (the one that creates the thread) blocks in the down operation below until the thread has reached theup() operation.*/
init_MUTEX_LOCKED(&kthread->startstop_sem);
/* store the function to be executed in the data passed to the launcher */
kthread->function=func;
kernel_thread((int (*)(void *))kthread->function, (void *)kthread, 0);
/* wait till it has reached the setup_thread routine */
down(&kthread->startstop_sem);
}
/* stop a kernel thread. Called by the removing instance */
void stop_kthread(kthread_t *kthread)
{
if (kthread->thread == NULL)
{ printk("stop_kthread: killing non existing thread!/n"); return; }
/* initialize the semaphore. We lock it here, the leave_thread call of the thread to be terminated will unlock it. As soon as we see the semaphore unlocked, we know that the thread has exited. */
init_MUTEX_LOCKED(&kthread->startstop_sem);
/* We need to do a memory barrier here to be sure that the flags are visible on all CPUs. */
mb();
/* set flag to request thread termination */
atomic_inc(&kthread->terminate);
/* We need to do a memory barrier here to be sure that the flags are visible on all CPUs. */
mb();
kill_proc(kthread->thread->pid, SIGKILL, 1);
/* block till thread terminated */
down(&kthread->startstop_sem);
} /*
initialize new created thread. Called by the new thread. */
void init_kthread(kthread_t *kthread, char *name)
{
/* fill in thread structure */
kthread->thread = current;
#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,6,0)
/* reparent it to init and give name of “Kthread_example” to the new thread */
daemonize("Kthread_example");
#else
daemonize();
strcpy(current->comm, " Kthread_example");
#endif
/* set signal mask to what we want to respond */
allow_signal (SIGKILL| SIGINT| SIGTERM);
/* initialise wait queue */
init_waitqueue_head(&kthread->queue);
/* initialise termination flag */
atomic_set( &kthread->terminate, 0 );
/* tell the creator that we are ready and let him continue */
up(&kthread->startstop_sem);
}
/* cleanup of thread. Called by the exiting thread. */
void exit_kthread(kthread_t *kthread)
{
/* we are terminating */
kthread->thread = NULL;
/* notify the stop_kthread() routine that we are terminating. */
up(&kthread->startstop_sem);
}
static int __init init(void)
{
printk("Kernel thread Lib start!/n");
return 0;
}
static void __exit finish(void)
{
printk("Kernel thread Lib Goodbye!/n");
}
module_init(init); module_exit(finish);
EXPORT_SYMBOL(start_kthread);
EXPORT_SYMBOL(stop_kthread);
EXPORT_SYMBOL(init_kthread);
EXPORT_SYMBOL(exit_kthread);
MODULE_LICENSE("GPL");
7.4thread_drv.c
新建线程函数体为example_thread()
执行loop (for(;;)),在内部等待监测事件或者信号,收到事件后执行正常任务,收到信号时检测退出标志。
#include #include #include #if defined(MODVERSIONS) #include #endif #include #include #include #include #include "kthread.h" #define NTHREADS 5 /* the variable that contains the thread data */ kthread_t example[NTHREADS]; /* prototype for the example thread */ static void example_thread(kthread_t *kthread); /* load the module */ int init_module(void) { int i; /* create new kernel threads */ for (i=0; i queue, HZ); /* We need to do a memory barrier here to be sure that the flags are visible on all CPUs. */ mb(); /* here we are back from sleep, either due to the timeout (one second), or because we caught a signal. */ if (atomic_read(&kthread->terminate)) { /* we received a request to terminate ourself */ break; } /* Insert your own code here */ printk("example thread: thread woke up/n"); }
/* here we go only in case of termination of the thread *//* cleanup the thread, leave */ exit_kthread(kthread); /* returning from the thread here calls the exit functions */ }
7.5Makefile
# set to your kernel tree KERNEL = XXXXX # get the Linux architecture. Needed to find proper include file for CFLAGS ARCH= XXXXX
# set cross compile tools
CROSS_COMPILE := XXXXX
#
# Include the make variables (CC, etc...)
#
LD= $(CROSS_COMPILE)ld
CC= $(CROSS_COMPILE)gcc
# set default flags to compile module CFLAGS = -D__KERNEL__ -DMODULE -I$(KERNEL)/include CFLAGS+= -Wall -Wstrict-prototypes -O2 -fomit-frame-pointer -fno-strict-aliasing # get configuration of kernel include $(KERNEL)/.config # modify CFLAGS with architecture specific flags include $(KERNEL)/arch/${ARCH}/Makefile # enable the module versions, if configured in kernel source tree ifdef CONFIG_MODVERSIONS CFLAGS+= -DMODVERSIONS -include $(KERNEL)/include/linux/modversions.h endif
# enable SMP, if configured in kernel source tree ifdef CONFIG_SMP CFLAGS+= -D__SMP__ endif
OBJS =XXXX.ko
all : $(OBJS)
$(OBJS) : thread_drv.o XXXX.o YYYY.ko
$(LD) -r $^ -o $@clean: rm -f *.o
8参数文献
内核线程的使用
tarius.wu,内核线程、轻量级进程、用户线程
唐宇,关于内核线程(kernel_thread)
Linux内核线程的解释,http://blog.sina.com.cn/s/blog_4980e953010003bb.html
Linux Kernel Threads in Device Drivers―― http://www.scs.ch/~frey/linux/kernelthreads.html
Sreekrishnan Venkateswaran,Kernel Threads,http://www.linux-mag.com/id/2195
How to kill a kernel thread?,http://kerneltrap.org/node/6207
Can I start kernel threads from a kernel module?,http://www.kasperd.net/%7Ekasperd/comp.os.linux.development.faq
Dinesh Ahuja,Linux Kernel Series: Linux Kernel Threads