内核线程
内核线程只运行在内核态,不受用户态上下文的拖累。
处理器竞争:可以在全系统范围内竞争处理器资源;
使用资源:唯一使用的资源是内核栈和上下文切换时保持寄存器的空间
调度:调度的开销可能和进程自身差不多昂贵
同步效率:资源的同步和数据共享比整个进程的数据同步和共享要低一些。
轻量级进程
轻量级进程(lwp)是建立在内核之上并由内核支持的用户线程,它是内核线程的高度抽象,每一个轻量级进程都与一个特定的内核线程关联。内核线程只能由内核管理并像普通进程一样被调度。
轻量级进程由clone()系统调用创建,参数是clone_vm,即与父进程是共享进程地址空间和系统资源。
与普通进程区别:lwp只有一个最小的执行上下文和调度程序所需的统计信息。
处理器竞争:因与特定内核线程关联,因此可以在全系统范围内竞争处理器资源
使用资源:与父进程共享进程地址空间
调度:像普通进程一样调度
用户线程
用户线程是完全建立在用户空间的线程库,用户线程的创建、调度、同步和销毁全又库函数在用户空间完成,不需要内核的帮助。因此这种线程是极其低消耗和高效的。
处理器竞争:单纯的用户线程是建立在用户空间,其对内核是透明的,因此其所属进程单独参与处理器的竞争,而进程的所有线程参与竞争该进程的资源。
使用资源:与所属进程共享进程地址空间和系统资源。
调度:由在用户空间实现的线程库,在所属进程内进行调度
linux使用的线程库
linuxthreads是用户空间的线程库,所采用的是线程-进程1对1模型(即一个用户线程对应一个轻量级进程,而一个轻量级进程对应一个特定的内核线程),将线程的调度等同于进程的调度,调度交由内核完成,而线程的创建、同步、销毁由核外线程库完成(linuxthtreads已绑定到glibc中发行)。
在linuxthreads中,由专门的一个管理线程处理所有的线程管理工作。当进程第一次调用pthread_create()创建线程时就会先创建(clone())并启动管理线程。后续进程pthread_create()创建线程时,都是管理线程作为pthread_create()的调用者的子线程,通过调用clone()来创建用户线程,并记录轻量级进程号和线程id的映射关系,因此,用户线程其实是管理线程的子线程。
linuxthreads只支持调度范围为pthread_scope_system的调度,默认的调度策略是sched_other。
用户线程调度策略也可修改成sched_fifo或sched_rr方式,这两种方式支持优先级为0-99,而sched_other只支持0。
sched_other 分时调度策略,
sched_fifo 实时调度策略,先到先服务
sched_rr 实时调度策略,时间片轮转
sched_other是普通进程的,后两个是实时进程的(一般的进程都是普通进程,系统中出现实时进程的机会很少)。sched_fifo、sched_rr优先级高于所有sched_other的进程,所以只要他们能够运行,在他们运行完之前,所有sched_other的进程的都没有得到执行的机会。
【摘要】本文首先介绍了进程和线程的区别,接着分析了内核线程、轻量级LWP线程以及常见的用户线程的特点,同时介绍了内核线程和进程的区别。分析了创建内核线程kernel_thread函数的实现过程,介绍了一个在驱动中使用内核线程的实例。最后针对内核线程创建销毁的特点,给出了通用的内核线程操作函数API,使用该API可在自己的驱动或内核代码中方便的使用内核线程。
【关键词】线程,进程,内核线程,用户线程,LWP,kernel_thread,daemonize,kill_proc, allow_signal,SIGKILL
1 线程和进程的差别
在现代操作系统中,进程支持多线程。进程是资源管理及分配的最小单元;而线程是程序执行的最小单元。一个进程的组成实体可以分为两大部分:线程集和资源集。进程中的线程是动态的对象,代表了进程指令的执行过程。资源,包括地址空间、打开的文件、用户信息等等,由进程内的线程共享。线程有自己的私有数据:程序计数器,栈空间以及寄存器。
现实中有很多需要并发处理的任务,如数据库的服务器端、网络服务器、大容量计算等。传统的UNIX进程是单线程的,单线程意味着程序必须是顺序执行,不能并发,即在一个时刻只能运行在一个处理器上,因此不能充分利用多处理器框架的计算机。
如果采用多进程的方法,则有如下问题:
2 fork一个子进程的消耗是很大的,fork是一个昂贵的系统调用。
2 各个进程拥有自己独立的地址空间,进程间的协作需要复杂的IPC技术,如消息传递和共享内存等。
线程推广了进程的概念,使一个进程可以包含多个活动(或者说执行序列等等)。多线程的优点和缺点实际上是对立统一的。使用线程的优点在于:
2 改进程序的实时响应能力;
2 更有效的使用多处理器,真正的并行(parallelism);
2 改进程序结构,具备多个控制流;
2 通讯方便,由于共享进程的代码和全局数据;
2 减少对系统资源的使用。对属于同一个进程的线程之间进行调度切换时不需要调用系统调用,因此将减少额外的消耗,往往一个进程可以启动上千个线程也没有什么问题。
缺点在于:
由于各线程共享进程的地址空间,因此可能会导致竞争,因此对某一块有多个线程要访问的数据需要一些同步技术。
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往往很小。
2 PID TTY STAT TIME COMMAND
2 1 ? S 0:01 init [3]
2 3 ? SN 0:00 [ksoftirqd/0]
2 5 ? SN 0:00 [ksoftirqd/1]
2 6 ? S
2 7 ? S
2 8 ? S
2 9 ? S
2 10 ? S
2 11 ? S 0:00 [khubd]
2 35 ? S 0:42 [pdflush]
2 36 ? S 0:02 [pdflush]
2 38 ? S
2 39 ? S
2 37 ? S 0:19 [kswapd0]
2 112 ? S 0:00 [kseriod]
2 177 ? S 0:00 [scsi_eh_0]
2 178 ? S 0:00 [ahc_dv_0]
2 188 ? S 0:00 [scsi_eh_1]
2 189 ? S 0:00 [ahc_dv_1]
2 196 ? S 2:31 [kjournald]
2 1277 ? S 0:00 [kjournald]
2 1745 ? Ss 0:02 syslogd -m 0
2 1749 ? Ss 0:00 klogd -x
2 1958 ? Ss 0:13 /usr/sbin/sshd
2 2060 ? Ss 0:00 crond
2 2135 tty2 Ss+ 0:00 /sbin/mingetty tty2
2 2136 tty3 Ss+ 0:00 /sbin/mingetty tty3
2 2137 tty4 Ss+ 0:00 /sbin/mingetty tty4
2 2138 tty5 Ss+ 0:00 /sbin/mingetty tty5
2 2139 tty6 Ss+ 0:00 /sbin/mingetty tty6
2 23564 ? S 0:00 bash
2 25605 ? Ss 0:00 sshd: peter [priv]
2 25607 ? S 0:00 sshd:
[email=peter@pts/2]peter@pts/2[/email]
3.1 内核线程
2 events 处理内核事件 很多软硬件事件(比如断电,文件变更)被转换为events,并分发给对相应事件感兴趣的线程进行响应
2 ksoftirqd 处理软中断 硬件中断处理往往需要关中断,而这个时间不能太长,否则会丢失新的中断。所以中断处理的很大一部分工作移出,转给任劳任怨的ksoftirqd在中断之外进行处理。比如一个网络包,从网卡里面取出这个过程可能需要关中断,但是TCP/IP协议处理就不必关中断了
2 kblockd 管理磁盘块读写
2 kjournald Ext3文件系统的日志管理 通常每个 _已mount_ 的 Ext3分区会有一个 kjournald看管,各分区的日志是独立
2 pdflush dirty内存页面的回写 太多dirty的页面意味着风险,比如故障时候的内容丢失,以及对突发的大量物理内存请求的响应(大量回写会导致糟糕的响应时间)
2 kswapd 内存回收 确保系统空闲物理内存的数量在一个合适的范围
2 aio 代替用户进程管理io 用以支持用户态的AIO
3.2 用户进程
2 crond 执行定时任务
2 init 为内核创建的第一个线程。引导用户空间服务,管理孤儿线程,以及运行级别的转换
2 mingetty 等待用户从tty登录
2 bash shell进程,一个命令行形式的系统接口;接受用户的命令,并进行解释、执
2 sshd ssh登录、文件传输、命令执行 等操作的服务进程
2 klogd 从内核信息缓冲区获取打印信息。内核在发现异常的时候,往往会输出一些消息给用户,这个对于故障处理很有用
2 syslogd 系统日志进程
2 udevd 支持用户态设备操作 (userspace device)
4 内核线程与用户进程的关系
内核线程也可以叫内核任务,例如,磁盘高速缓存的刷新,网络连接的维护,页面的换入换出等等。在Linux中,内核线程与普通进程有一些本质的区别,从以下几个方面可以看出二者之间的差异:
2 内核线程能够访问内核中数据,调用内核函数,而普通进程只有通过系统调用才能执行内核中的函数;
2 内核线程只运行在内核态,而普通进程既可以运行在用户态,也可以运行在内核态;
2 因为内核线程指只运行在内核态,因此,它只能使用大于PAGE_OFFSET(3G)的地址空间。另一方面,不管在用户态还是内核态,普通进程可以使用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(®s, 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, ®s, 0, NULL, NULL);
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
#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文件。
2 start_kthread:
建立一个内核线程。可在任何进程上下文中执行,down()阻塞在同步信号量semaphore上直至新建立线程启动后执行init_kthread,其调用up()通知start_kthread新线程建立成功并运行。
2 stop_kthread:
清除一个内核线程。可在除待清楚进程之外的任何进程上下文中执行。设置退出标志供所建线程检测并发送SIGKILL信号给该线程,阻塞,直至其调用exit_kthread通知killer其已经安全退出。
2 init_kthread:
初始化新建立的线程环境,在新建线程循环外部执行。其将调用daemonize更新父进程为init进程便于回收资源,设置待捕捉的信号以便killer可以清除该线程。同时设置新线程的名称,清除退出标志,up()通知建立者新线程成功建立。
2 exit_kthread:
收到中止信号后调用,退出。up()通知stop_kthread()其已经退出。
7.2 kthread.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_t terminate;
/* 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.3 kthread.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 the up() 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.4 thread_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.5 Makefile
# 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
这几天由于工作需要,琢磨了一下Linux下的多线程的相关资料。Linux下最常用的多线程支持库为Pthread库,它是glibc库的组成部分。但是关于Pthread的说明文档非常缺乏,特别是对POSIX多线程规范的介绍以及pthread库中多线程实现方式的介绍实在是少之又少。而多线程编程对于系统程序员而言是必须掌握的技术,因此总是让学习中的程序员觉得头痛不以。我自己也没有太多多线程编程的经验,在这里只是把自己收集到的一些关于Linux上多线程还算新的资料进行汇总来抛砖引玉,以便相互学习交流。
这里顺便提一下市面上有的一本介绍多线程的书《Posix 多线程编程》,它是英文版《Programming with POSIX Muiltthread》中译本,这也是半年前我所能找到的唯一专题介绍多线程编程的书。我个人感觉这本书的前面1/3之一的内容写的还是不错的,但是后面的东西就非常晦涩并且有很多明显的文字错误。看看这本书的翻译者是好几个人,估计每个人的翻译能力不同造成了这本书的虎头蛇尾。 因此我不建议大家去买这本书作为圣经收藏。这本书前半步的内容主要围绕Posix的多线程,介绍的比较精彩的就是几个多线程编程模型,把多线程的互斥和同步机制介绍的挺酣畅的,推荐一看。 这些内容并非这本书首创,早在《UNIX网络编程》第二卷进程间通信就有了这些经典的介绍,但是能系统的把这些机制结合到多线程编程中来还是有可圈可点之处的。此外毕竟《UNIX网络编程》两卷内容太老,书也太厚了,并不是大多数程序员所能坐下来细细看的。这里我还想表达一下对微软在技术上的不足斥责。在msdn中platform sdk部分中的windows多线程编程的内容真是简陋的可笑,只有傻兮兮的建立和退出线程的函数,关于互斥,条件的介绍一概全无。只能在它的sample代码中自己去找,sample代码里面的线程同步方式居然是做一个死循环来死等,也不知道它把windows卖这么多钱是干什么吃的。 MFC中多线程的封装倒是看上去像那么一回事情了,但是我想象不出在如此简陋的系统api上微软到底是如何实现出MFC上线程功能的。拥护windows的人不要在这里砸鸡蛋,最好也能写一篇windows上的多线程介绍除了。这比砸鸡蛋来得有意义多了。 好了,书归正传继续说Linux上的多线程。
在Linux上,从内核角度而言,基本没有什么线程和进程的区别--大家都是进程。一个进程的多个线程只是多个特殊的进程他们虽然有各自的进程描述结构,却共享了同一个代码上下文。在Linux上,这样的进程称为轻量级进程Light weight process。致此,就是关于线程的总体概念了,我们往往就在了解这个概念的情况下开始我们的多线程编程之旅。这对于多线程编程入门已经足够了,然而事实上线程却要复杂的多。 首先多线程间的优先级调度,内存资源(栈)分配和信号投递就不是简单的共享同一个进程代码上下文所能所能解决的。其次,效率的问题:如何有效的使用多cpu资源(2.4内核的多线程就无法使用多个cpu,一个进程的线程都被限制在同一个cpu上运行)。因此多线程库Pthread的实现并不是一件简单的事情,它建立在特有的线程模型之上。
在Linux 2.4内核中, Linux内核中使用了一个内核线程来处理用户态进程中的多个线程的上下文切换(线程切换)。由于内核中并没有什么线程组的概念,即一个进程的多个线程,因此必须依靠在pthread库中实现一个额外的线程来管理其他用户线程(即用户程序生成的线程)的建立,退出,资源分配和回收以及线程的切换。由于当时硬件并没有线程寄存器之类的冬冬来支持多线程,因此线程的切换性能和低下,并且需要引入复杂的机制在进程的栈中为各个线程划分出各自的栈数据所在位置,并且在切换时进行栈数据拷贝。而最大的问题是内核中缺乏对线程间的同步机制的支持,因此pthread库不得不在底层依靠信号方式来实现同步,因此线程互斥中的互斥量操作和条件量操作都转换为进程的信号操作。pthread的实现中充斥了极其复杂的信号操作。大家都知道信号本身是低速的通信方式,因此势必拖慢了线程的实际性能。最后的问题就是信号处理,还有由于内核对线程的无知,必须由管理线程来接收信号后投递给相应的线程,一方面是效率低,另外一方面由于信号产生的不确定性(比如读取一个文件的时候突然出错了),要准确投递所有的信号给正确的线程难以保证。
而在IA-32硬件结构中,出现了对线程寄存器的支持,因此Pthread的线程上下文切换速度有了很大提高。但是由于硬件限制局限,线程的数量必须小于8192个,反正我是觉得已经很多了。
于是从2.5代码开始Linux内核采用了NPTLNative Posix Thread Library)方式。NPTL的设计思想初稿可参考nptl-design.pdf(http://people.redhat.com/drepper/nptl-design.pdf)
首先在IA-32和x86-64位体系结构上能实现任意数量的线程数量。通过引入了TLS系统调用可以建立多个GDT全局描述符表,每个cpu维护一个描述符表,每个表项存放一个线程。
其次,clone系统调用优化了线程的建立和结束功能。也不再需要额外的调度线程的帮助就可以回收线程资源了。
其三,信号投递由内核完成,而不再需要额外的用户态管理线程的帮助,而严重错误信号之间结束整个进程。
其四,引入了新的退出系统调用exit_group()。原来的exit保留用于退出单个线程,exit_group用于退出整个进程。
其五, 新的exec调用会先结束到一个进程中的所有线程后再载入新程序的执行,而不是只结束调用的线程。
其六,所有线程的资源使用情况(cpu资源,内存资源)会报告给整个进程,而不再是只报告给初始化线程
其七,proc文件系统中只显示初始化线程的信息,而不再是所有线程的信息(上万个线程会把proc文件系统拖死)
其八, 支持线程脱离, 执行Pthread_join的线程不需要再执行no wait。
其九,由内核来维护初始化线程(变成内核线程了),并在proc文件系统中显示其状态,并维护直到所有线程退出来保证信号的投递。
其十,内核支持无限制的线程数量。
最后,允许pthread_join在子线程已死之后返回,即pthread_join的返回和子线程状态变成异步的了,提高了性能。
根据报告,NPTL中线程的启动和中止时间消耗只有Linuxthread的大约1/8,当线程数量急遽增加的时候,消耗时间的差异更加明显。
在线程间同步试验中,频繁进出临界区的时间消耗只有原来的一半。
更多的用户测试报告可以看 http://kerneltrap.org/node/422
至于如何在开发中使用NPTL可参考Migrating to Linux kernel 2.6 -- Part 5: Migrating apps to the 2.6 kernel and NPTL(http://linuxdevices.com/articles/AT6753699732.html)。需要做的事情有这么几件。
1:使用2.6的内核的系统平台
2:确定你的gcc支持NPTL
用# getconf GNU_LIBPTHREAD_VERSION命令来查看gcc的编译时的对多线程的支持方式
如果返回的是linuxthreads-0.10,说明你的gcc不支持NPTL
如果返回的是nptl-0.60这样的信息,说明你的gcc能用来编译新的NPTL
3:重新在这样的系统环境中编译你的程序,不需要改变程序中对pthread的调用(但是某些函数被取消了)