进程、轻量级进程(LWP)、线程

来源:互联网 发布:水乳推荐20到25岁 知乎 编辑:程序博客网 时间:2024/05/16 05:19

转:进程、轻量级进程(LWP)、线程

转:关于进程、线程和轻量级进程的一些笔记

进程、轻量级进程(LWP)、线程

  • 进程:程序执行体,有生命期,用来分配资源的实体
  • 线程:分配CPU的实体。
    •   用户空间实现,一个线程阻塞,所有都阻塞。
    •   内核实现,不会所用相关线程都阻塞。用LWP实现,用线程组表示这些线程逻辑上所属的进程。
进程描述符
  • 进程描述符(简称pd, process descriptors),结构体是:task_struct
    •   数据较多,存放在kenerl的动态内存空间。
    •   pd的引用放在thread_info中,
      •    thread_info与内核栈,放在一个8K空间(它的地址8K对齐)。内核程序使用的栈空间很小。
      •    thread_info在底部,内核栈在顶部向下增长。
        •     好处:多CPU时方便,每个CPU根据自己的栈指针就可以找到当前的pd (以后用current表示当前CPU运行的进程描述符)。
          •      esp(内核栈指针)低8位置零,就是thread_info地址。
      •    每进程有自己的thread_info, (分配释放函数: alloc_thread_info, free_thread_info)
  • 描述符的内容
    •   相关的ID (一个4元素数组)
      •    进程ID (PID)
        •     PID按创建顺序连续增长,到最大值后从最小值开始。
        •     0号进程:交换进程(swapper)
        •     有PID可用位图,表示那一个PID可用,至少占一个页。
      •    线程组ID(tgid),用LWP实现多线程支持
        •     多进程时,进程id,就是线程组id, 也就是组长的pid(LWP)。 getpid() 取的是线程组的id(tgid), 也是组长的pid.
        •     单线程时,pid = gid。所以getpid,也是真正的pid.
      •    进程组ID(pgrp)。
      •    回话的ID(session).
        •     组ID,都是组长的PID。FIXME: 但pb也有各组长的PID
          •      线程组长:tgid
          •      进程组长:signal->pgrp ,
          •      会话长:signal->session
      •    管理ID数据结构——哈希表管理 (利用id找到所用相关的pd,方便)。
        •     一个哈希表数组(pid_hash),存放四个哈希表, 每一个表代表一类id (pid, tgid, pgrp, session)
        •     每个哈希表的由数组(索引为哈希值)和二维链表(嵌入到进程描述符内的pids中)实现
          •      第一维链表:哈希冲突链表。
          •      第二维链表:要查找的值相同的链表, 叫per-PID list(同一组的所有线程,同一组的所有进程,同一会话的所有进程);
      •    进程组ID(pgrp), 回话ID(session)在共享信号的数据结构里。因为同一进程内的所有LWP,这两个ID都是一样的
  •  
    •   家族关系:由pd里的链表(下级)和pd指针(上级)实现
      •    关系:
        •     亲生父亲:创建自己的进程,或是托孤进程(创建自己的进程死了)。
        •     父亲:自己死时要发信号告知的。一般是亲生父亲,有时是监控自己的进程 (调用ptrace)
        •     孩子:
        •     兄弟:
      •    监控(自己起的名字,类似于监护。由于管理方式相同,也归为家族关系)
        •     监控的进程列表:ptrace_children
        •     被监控的其他进程:ptrace_list (类似于被监控的兄弟)
      •    在链表里为了管理方便:
        •     最大儿子的兄弟是父亲
        •     最小儿子的弟弟也是父亲
        •     父亲保管最大儿子,和最小儿子
  •  
    •   进程资源及资源限制:
      •    CPU相关:
        •     占用CPU总时间
        •     用户的最大进程数
      •    内存相关:
        •     进程地址空间
        •     锁住内存大小
        •     进程页数 (只有记录,没有限制)
        •     堆大小,栈大小
      •    资源相关:
        •     文件:
          •      core dump大小
          •      最大文件大小
          •      打开文件个数
        •     进程同步与通信
          •      锁数目,
          •      悬挂信号数据
          •      在消息列队中占的大小
      •    相关数据结构 和 处理流程
        •     pd->sigal->rlim 是一个表示进程资源使用情况以及限制的结构 的数组。
        •     表示进程资源使用情况以及限制的结构:包含当前值,最大值两个数值。
      •    只有超级用户才能增大资源限制。
      •    一般用户登陆时:
        •     kernel创建root进程,减少limit,
        •     建一个 shell子进程,继承limit.
        •     把shell进程的用户,改成登陆的那个用户
  •  
    •   进程状态(state)
      •    运行,TASK_RUNNING
        •     组织pd的结构:就绪进程链:
          •      一个CPU一组链表,每个链表表示一种优先级。
      •    阻塞
        •     可中断阻塞,TASK_INTERRUPTIBLE
          •      可被硬件中断,“释放资源”事件,信号唤醒。
        •     不可中断阻塞,TASK_UNINTERRUPTIBLE
          •      可被硬件中断,“释放资源”事件,唤醒。
          •      但不能被信号唤醒。可用于驱动程序中。
        •     组织pb的结构:等待列队: 每一类事件一个列队,用内嵌链表实现(虽然没列出内嵌链表节点)
          •      列队头:
            •       自旋锁:防止有一个主函数和中断函数同时操作列队。
          •      列队节点:
            •       独占标志:表示该进程是否要独占资源 (不再唤醒别的进程)
            •       指向pd的指针
            •       用于唤醒进程的回调函数。(提供进程的执行机会,是否操作等待列队由用户决定)
      •    停止
        •     停止TASK_STOPPED
          •      被信号停止
        •     追踪TASK_TRACED
          •      该进程被一个调试进程监控以后,收到任何一个信号就进入该状态
        •     组织pb的结构:FIXME: 信号的等待列队?
      •    退出
        •     退出_僵尸EXIT_ZOMBIE
          •      进程终止,资源没有被回收(父进程要用,没有调wait系列函数)
        •     退出_死亡EXIT_DEAD
          •      进程终止,资源正在被回收(父进程要用,没有调wait系列函数)。
          •      一旦资源回收完成,进程描述符也就被回收了。
          •      它防止该进程再次被wait.
        •     组织pb的结构:不挂到队列上,只在家族关系中,等待父进程收回资源
 程控制 :
  • 阻塞(current阻塞到某个列队上):
    •   基本流程
      •    临时生成一个列队节点,初始化。
      •    改变current的状态,放入节点,挂到列队上。
      •    调度 (=====》至此,阻塞完成。 一旦被别的进程唤醒====》从调度函数中返回)
      •    从等待列队上摘除节点。
    •   变化:
      •    将挂列队、调度、从列队删除三步拆开,便于灵活处理。
      •    可中断的、限时、独占的函数类似。只不过进程状态、调度函数、独占标志不同。
      •    非独占的从列队开始添加,独占的从末尾添加。(但一个列队内既有独占的,又有非独占的等待进程,很少见)
  • 唤醒:
    •   基本流程
      •    唤醒一个进程:调用节点里的回调函数
      •    唤醒的时候从列队开头依次唤醒,直到唤醒一个独占的后停止。
    •   变化
      •    是否只唤醒可中断的进程. (_interruptible后缀)
      •    唤醒的独占进程的数目(1个,多个(_nr后缀),所有(_all后缀))
      •    唤醒后是否不检查优先级,马上给予CPU (有_sync的不检查优先级)。
  • 进程切换
    •   切换pgd (全局页目录),此章不讨论。
    •   切换内核栈,硬件上下文
      •    硬件上下文,就是CPU的寄存器。
        •     一部分(大多数CPU寄存器(除了通用寄存器))在pd中保存(task_struct->thread, 类型是thread_struct),
        •     一部分(通用寄存器)保存在内核栈中.
      •    原来用硬件指令()保存CPU信息。后来改成软件(一个个MOV指令)
        •     容易控制,可以挑选信息保存,便于优化。不保存的做其他用(如:进程间传递)
          •           far jmp:跳至目标进程的TSSD。而linux是每个CPU一个TSS,不是每进程一个
        •     对于一些寄存器(ds、es)可以检查值。
        •     与用硬件指令保存时间差不多。
    •   switch_to 宏
      •    三个参数:
        •     prev: 要换走的进程,一般是当前进程
        •     next: 要换到的进程。
        •     last: 传出参数。当前进程再次被换到时,最后一个占用CPU的进程。(prev指向的进程 就是 next指向的进程 的last)
      •    步骤:
        •     栈切换, 完成后就是在新进程的上执行了:
          •      保存prev(放在eax)
          •      eflags,ebp入内核栈;
          •      保存并装载新的esp (旧的esp放到prev->thread.esp,新的esp是next->thread.esp)
            •       此时current就是新的esp所指的thread_info内的task指针
        •     设置返回地址:
          •      prev进程以后得到执行时的__switch_to的返回地址: __switch_to后的第一条指令, 放入prev->thread.eip,
          •      准备next进程的从__switch_to返回的地址: next->thread.eip入栈.
        •     调用__switch_to ()函数,该函数动作如下:
          •      更新CPU的相关信息(tss和gdt):
            •       存next->thread.esp0(内核栈低)到本地TSS.esp0中。
            •       所在CPU的全局段表里的TLS段, 设成next进程的.
            •       更新tss的I/O位图.
          •      更新CPU的寄存器(pd->thread (tss) 与 CPU寄存器交换数据):
            •       保存FPU, MMX, XMM寄存器, 先不装载以后需要时通过中断装载(TODO: )
            •       保存prev的fs, gs寄存器. 装载next的
            •       装载next的debug寄存器(debug寄存器一个8个, 进程切换时只需6个)
          •      返回
            •       prev放入eax (prev就是新进程的last)
            •       ret
        •     ret返回的地址: (__switch_to之前被存入栈中, __switch_to ret时进入eip)
          •      如果是next新进程, next->thread.eip是iret_from_fork.
          •      如果next不是新进程:
            •       弹出ebp, elfags
            •       把eax放入last变量 (prev就是next进程的last)
  • 任务状态段(一个存CPU状态的数组,tss_struct init_tss[])
    •     每个CPU用段上的一个元素。(FIXME: 用于:用户模式要进入内核模式时,设置相应寄存器)
      •       TSS上存内核栈地址。CPU上的程序从用户模式转到内核模式,设置esp。
      •       TSS存I/O端口许可位图。用户模式程序用到I/O时,检查有无权限
      •       所以,进程切换时,要保存的寄存器在pd->thread中。
        •         thread_struct不是thread_info。thread_info中只有少量的数据或指针, 用于通过esp快速定位数据
    •     进程切换时,更新TSS上的信息。
      •       CPU控制单元再从TSS上取需要的信息。
      •       即反应了CPU的当前进程情况,又不需要维护所有进程的状态数据。
    •     TSS的描述符在GDT里。
      •       TSSD:任务状态段描述符 (其实应该叫任务状态描述符,每个TSSD,表示一个CPU的状态, FIXME: :具体以源码为准)
      •       CPU原始设计,每个进程一个TSS元素。
      •       linux设计,每个CPU一个TSS元素。
      •       cpu里的tr寄存器,保存着自己的TSSD(即init_ttss[cpu_id]),不用总上gdt里去取。




    • 进程创建: clone, fork, vfork系统调用
      •   clone系统调用
        •    参数:
          •     执行函数(fn), 参数(arg)
          •     flags|死亡时给父进程发的信号 (clone_flags): 以下介绍clone_flags
            •      资源共享
              •       段,页,打开文件共享:
                •        页表(不是页, CLONE_VM),
                •        打开文件(clone_files),
                •        建一个新tls段(clone_settls)
              •       路径和权限设置:
                •        clone_fs: 共享根目录, 当前目录, 创建文件初始权限.
                •        clone_newns: 新的根路径, 自己的视野看文件系统
              •       线程通信
                •        clone_sighand: 信号处理action, 阻塞和悬挂的信号
                •        clone_sysvsem: 共享undoable信号量操作
            •      进程关系
              •       同父: clone_parent 创建进程与新进程是兄弟 (同父), 新进程不是创建进程的子进程
                •        为了方便期间, 以下讨论暂时不考虑这一因素(它很容易实现), 认为创建进程就是父进程
              •       同一个线程组: clone_thread. 属于同一个进程(线程组)
              •       都被trace: clone_ptrace
              •       子进程不被trace: clone_untrace (内核设置, 覆盖clone_ptrace)
            •      返回tid
              •       向父进程返回tid: clone_parent_settid
              •       向子进程返回tid: clone_child_settid
            •      子进程的状态:
              •       子进程开始就stop: clone_stopped
            •      进程死亡或exec通知:
              •       启动内核机制: 如果子进程死亡或exec, 它自己空间内的tid(*ctid)清零, 并唤醒等待子进程死亡的进程.
          •     赋给子进程的资源
            •      子进程的栈(父进程alloc的内存地址)
            •      线程局部仓库段(tls)
          •     返回子进程tid的地址
            •      父进程用户空间内的地址
            •      子进程用户空间的地址
      •   clone, fork, vfork实现方式
        •   大致相同:
          •     系统调用服务例程sys_clone, sys_fork, sys_vfork三者最终都是调用do_fork函数完成.
            •     do_fork的参数与clone系统调用的参数类似, 不过多了一个regs(内核栈保存的用户模式寄存器). 实际上其他的参数也都是用regs取的
        •    区别在于:
          •     clone:
            •      clone的API外衣, 把fn, arg压入用户栈中, 然后引发系统调用. 返回用户模式后下一条指令就是fn.
            •      sysclone: parent_tidptr, child_tidptr都传到了 do_fork的参数中
            •      sysclone: 检查是否有新的栈, 如果没有就用父进程的栈 (开始地址就是regs.esp)
          •     fork, vfork:
            •      服务例程就是直接调用do_fork, 不过参数稍加修改
            •      clone_flags:
              •       sys_fork: SIGCHLD|0;
              •       sys_vfork: SIGCHLD| (clone_vfork | clone_vm)
            •      用户栈: 都是父进程的栈.
            •      parent_tidptr, child_ctidptr都是NULL.
      • 具体实现函数do_fork() (内核函数)的工作流程:
        •   分配PID, 确定子进程到底是否traced.
          •    分配空闲的PID
          •    确定clone_ptrace位. (确定子进程到底要不要被trace, 而不是参数所说的希望被trace)
            •     设置该位: 参数已设该位, 且创建线程被trace中
            •     清除该位: 父进程没有被trace, 或 clone_untrace已经设置.
        •   复制进程描述符(copy_process)
          •    检查clone_flags是否兼容, 是否安全
            •     clone_newns 与 clone_fs 互斥
            •     clone_sighand 是 clone_thread 的必要条件: 线程必须共享信号处理
            •     clone_vm 是 clone_sighand 的必要条件 : 共享信号处理, 首先要共享信号处理的代码(在进程页面里)
            •     附加的安全检查: security_task_create(clone_flags)
          •    复制进程描述符
            •     在父进程的thread_info里保存浮点寄存器: __unlazy_fpu()
            •     分配新的进程pd(alloc_task_struct), 并拷贝父进程pd
            •     分配新的thread_info(alloc_thread_info), 并拷贝父进程的thread_info.
            •     新的thread_info和新分配的pd 相互引, 新pd的引用计数设为2 (表示:新pd有用, 且不是僵尸进程)
          •    相关计数加1: (此处先相关计数检查, 都通过后再都加1)
            •     检查并增加: 用户拥有进程数, 系统总共进程数.
              •      一般来说, 所有进程的thread_info总和, 不超过物理内存的1/8
            •     新进程的可执行格式的引用计数(FIXME: pd里标有可执行个数吗)
            •     系统执行fork总数.
          •    进程pd的关键域的设置(顺序与源码可能不一致):
            •     进程关系
              •      设置父子关系 (parent, real_parent, 考虑被trace的情况)
              •      设置新pd的PID
              •      设置tgid, 线程组长的pd(pd->group_leader). (根据是不是线程组长, 即clone_thread位是否为0)
              •      加入PID哈希表(pid, tgid, 如果是进程组长加入pgid和sid表),(调attach_pid())
              •      拷贝tid到父进程的用户空间(parent_tidptr)
            •     拷贝资源(如果clone_flags没标明共享):
              •      文件,目录,内存:copy_files, copy_mm, copy_namespace,
              •      进程通信: copy_signal, copy_sighand, copy_semundo
            •     设置子进程的内核栈(thread_info), 内核态相关寄存器(thread_struct, 不知道这个结构的具体用处): copy_thread()
              •      子进程的thread_struct:
                •       esp, esp0 - 内核栈顶, 内核栈底
                •       eip - ret_from_fork()的地址 (用户态切到内核态的第一条指令)
                •       I/O许可位图 - 如果父进程有, 就拷贝一份过来
                •       TLS - 如果用户空间提供了TLS段, 拷贝过来
              •      设置子进程的内核栈:
                •       child_regs.esp = 传入的栈地址参数;
                •       child_regs.eax = 0, 给用户态的返回值是0
                •       清除thread_info中的, TIF_SYSCALL_TRACE位, 防止运行ret_from_fork时, 系统通知调试进程
                •       设置子进程的thread_info的cpuid
            •     设置调度信息(sched_fork())
              •      设置task_running状态,
              •      初始化调度参数(时间片),
              •      子进程禁止内核抢占(thread_info.preempt_cout = 1)
            •     其他:
              •      如果没有被trace,pd->ptrace = 0;
              •      设置pd->exit_signal:
                •       有clone_thread位: 设为参数clone_flags中的退出信号
                •       没有clone_thread位: 设为-1 (表示进程终止时, 该LWP不给父进程发信号)
              •      pd->flags: 清除PF_SUPERPRIV , 设置PF_FORKNOEXEC
              •      大内核锁 pd->lock_depth = -1
              •      exec次数: pd->did_exec = 0
              •      拷贝child_tidptr到pd->set_child_tid. 以备子进程开始执行时, 把tid放到自己内存空间的child_tidptr
          •    返回pd
        •   设置父子进程的运行状态, 调度信息
          •    设置子进程的状态.
            •     挂信号: 如果创建出来的是停止(clone_stopped)或被trace(pd->ptrace里有PT_PTRACE位)的进程, 悬挂一个SIGSTOP信号.
              •      只有debugger发出SIGCONT信号后, 才能进入运行状态
            •     设状态,入列队:如果有clone_stopped位, 子进程设为stopped状态; 否则调用wake_up_new_task(), 把子进程加入就绪列队:
              •      调整父进程和子进程的调度参数 (主要是时间片)
              •      如果父子在同一CPU上运行, 且页表不同享, 子进程在插在父进程前
                •       子进程很可能exec, 不与父进程共享页. 这样防止父进程无用的copy on write.
              •      如果不同CPU上运行, 或者共享页表, 子进程放在列队最后
          •    如果父进程处于被调试状态, 程通知调试器
            •     当前进程给debugger进程发信号, 告知自己创建了子进程; 并停止自己(进入traced状态), 使debugger运行.
              •      子进程的pid保存在current->ptrace_message中, 供debugger用
              •      调试器发信号, 使父进程继续后, 再进行下一步; 否则父进程一直处于traced状态
          •    设置父进程状态
            •     如果有clone_vfork, 把自己放到一个等待列队.
              •      内核处理完系统调用后, 会执行调度, 这样就阻塞父进程了.
              •      直到子进程释放了它的内存地址空间, 即子进程终止或exec新程序, 用信号唤醒父进程.
        •   返回子进程的pid.
        •   子进程被调度后,执行pd.thread.eip(ret_from_fork). 调用关系(=>): ret_from_fork=>schedule_tail=>finish_task_switch.
          •    schedule_tail的另一件事就是: 把pid保存到地址pd->set_child_tid (创建进程使的parent_tidptr)
          •    finish_task_switch的动作是: 装载内核栈保存的寄存器(regs->eax为0),返回到用户态。系统调用返回值就是eax(0)
      • 内核线程:
        •   只运行于kernel模式,只能访问大于3G的空间。而普通进程在内核模式时,能访问整个4G空间
        •   创建方法, 类似于clone
          •    准备返回地址fn: 构造一个regs. 里面有fn, args, __KERNEL_CS等. regs->eip是汇编函数kernel_thread_helper
          •    do_fork (flags|CLONE_VM|clone_untraced, 0, &regs, 0, NULL, NULL)
            •     创建线程, 与父进程共享页. 用上步构造的regs初始化新程的内核栈
          •    新线程被调度后. 由ret_from_fork, 用regs恢复寄存器, 开始执行kernel_thread_helper
          •    kernel_thread_helper: 把args压入栈, call fn(args, fn都寄存器中)
        •   典型的内核线程:
          •    进程0: 所有进程的祖先
            •     编译时存在.
              •      pd, 内核栈: init_task, init_thread_union
              •      资源: init_mm, init_files, init_fs.  信号: init_signals, init_sighand
              •      页表: swapper_gd_dir
            •     功能
              •      初始化系统数据,
                •       多CPU系统中, 开始时BIOS禁用其他CPU.
                •       初始化系统数据后, 进程0拷贝自己到其他CPU的调度列队上, 启动其他CPU, 所有的PID都是0.
              •      使能中断
              •      创建内核线程1, (函数是init)
              •      进入idle
          •    进程1:
            •     init函数 exec可执行文件init, 使内核线程变成了普通进程.
            •     管理其他进程, 称为托孤进程
          •    其他内核线程:
            •     执行工作列队:
              •      ksoftirqd: 执行 softlets
              •      kblockd: 执行工作列队 kblockd_workqueue, 定期激活块设备驱动
              •      keventd (又叫events): 处理工作列队 keventd_wq
            •     管理资源:
              •      kapmd: 电源管理
              •      kswapd: 交换进程, 用于回收内存资源
              •      pdflush: flush脏的磁盘缓存
      进程销毁
      • 进程终止
        •   系统调用
          •    整个进程终止: exit_group(), 由do_group_exit处理系统调用. c函数 exit()也是用的这系统调用
          •    某个线程终止: _exit(), 由do_exit处理. C函数中用到此系统调用的API: pthread_exit
        •   do_group_exit流程: (整个组内至少有一个线程调用它, 用于整组协调)
          •    检查线程组的退出过程是否启动: 检查signal_group_exit(线程组内的公共数据)是否非零. 如果没有启动, 执行一下操作来启动退出过程:
            •     设置启动标志signal_group_exit.
            •     存储终止码(exit_group的参数), 在current->signal->group_exit_cold
            •     向其他线程发SIG_KILL信号, (它们收到信号后, 调do_exit())
          •    调用do_exit, 使本线程退出
        •   do_exit流程:
          •    设置线程的终止标志, 退出码
            •     设置PF_EXITING, 标明要被终止
            •     设置pd->exit_code
              •      系统调用参数
              •      或是内核提供的错误码, 表示异常终止
          •    释放资源:
            •     删除该进程的定时器
            •     去除对资源的引用:
              •      exit_mm, __exit_files;
              •      __exit_fs(root路径,工作路径, 创建文件权限), exit_namespace(挂载的文件系统的视野);
              •      exit_thread(thread_struct), exit_sem,
          •    如果这个线程的函数实现了一种可执行格式, 可执行格式数的引用计数--; FIXME: 还没看到这块儿, 凑合翻译的不一定对
          •    改变父子关系, 并向父进程发信号, 改变自己的状态(exit_notify)
            •     托付终止线程创建的子进程:
              •      如果终止线程还有同组线程: 终止线程创建的子进程, 作为与同组线程的子进程.
              •      否则: 终止线程创建的子进程, 作为孤儿进程, 由init进程托管
            •     向父进程发信号
              •      exit_signal有意义 && 最后线程 :  发exit_signal
              •      否则:
                •       被trace : 发SIGCHLD
                •       没被trace : 不发信号
            •     僵尸自己或直接死亡,  并设置PF_DEAD位
              •      exit_signal没意义 && 没被trace : 直接死亡 (这种情况没有发信号)
                •       变成EXIT_DEAD状态,
                •       release_task() (后面介绍). pd引用计数变为1, 不会马上释放
              •      否则: 僵尸
                •       exit_signal有意义 || 被trace : 僵尸
              •      整理"僵尸"与"发临僵尸信号"的关系:
                •       将发信号的条件中"最后线程"去掉, 可简化为(exit_signal有意义)||(被trace) == (发信号)
                •       可得出后: (发信号) == (僵尸)
                •       又可推出: (没有trace && exit_signal有意义 && 不是最后进程) == (僵尸了,但没法信号) , 这种情况在移除死进程时, 会给其父进程发信号 (FIXME: 待验证)
          •    调度. 调度函数会忽略僵尸进程, 但会减少僵尸进程的pd的使用计数; 会检查PF_DEAD位, 把它变成exit_dead状态
      • 进程移除 TODO:


      Linux内核线程之深入浅出

      【摘要】本文首先介绍了进程和线程的区别,接着分析了内核线程、轻量级LWP线程以及常见的用户线程的特点,同时介绍了内核线程和进程的区别。分析了创建内核线程kernel_thread函数的实现过程,介绍了一个在驱动中使用内核线程的实例。最后针对内核线程创建销毁的特点,给出了通用的内核线程操作函数API,使用该API可在自己的驱动或内核代码中方便的使用内核线程。

       

      【关键词】线程,进程,内核线程,用户线程,LWPkernel_threaddaemonizekill_proc allow_signalSIGKILL

       

       

      1      线程和进程的差别

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

       

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

       

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

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

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

       

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

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

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

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

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

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

      缺点在于:

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

       

      2      线程的分类

      2.1     内核线程

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

       

      2.2     轻量级进程

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

       

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

       

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

      注:

      LWP的术语是借自于SVR4/MPSolaris 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的执行线程,lrARM的链接寄存器,当从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");

       

       

      7      通用的内核线程模块

      7.1     内核线程相关API

      本驱动实例展示如何创建及停止一个内核线程。以模块形式加载,相关接口函数export到内核中,供其他模块使用。

       

      内核版本 2.6.19,包含kthread.hkthread.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.2     kthread.h

      #ifndef _KTHREAD_H

      #define _KTHREAD_H

      #include <linux/config.h>

      #include <linux/version.h>

      #include <linux/kernel.h>

      #include <linux/sched.h>

      #include <linux/tqueue.h>

      #include <linux/wait.h>

      #include <asm/unistd.h>

      #include <asm/semaphore.h>

      /* 新建线程的所以相关信息 */

      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 <linux/config.h>

      #include <linux/version.h>

      #if defined(MODVERSIONS)

      #include <linux/modversions.h>

      #endif

      #include <linux/kernel.h>

      #include <linux/sched.h>

      #include <linux/tqueue.h>

      #include <linux/wait.h>

      #include <linux/signal.h>

      #include <asm/semaphore.h>

      #include <asm/smplock.h>

      #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 thedown 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 <linux/config.h> #include <linux/version.h> #include <linux/module.h> #if defined(MODVERSIONS) #include <linux/modversions.h> #endif #include <linux/kernel.h> #include <linux/string.h> #include <linux/errno.h> #include <linux/sched.h> #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 <NTHREADS; i++) start_kthread(example_thread, &example); return(0); } /* remove the module */ void cleanup_module(void) { int i; /* terminate the kernel threads */ for (i=0; i<NTHREADS; i++) stop_kthread(&example); return; } /* this is the thread function that we are executing */ static void example_thread(kthread_t *kthread) { /* setup the thread environment */ init_kthread(kthread, "example thread"); printk("hi, here is the kernel thread/n"); /* an endless loop in which we are doing our work */ for(;;) { /* fall asleep for one second */ interruptible_sleep_on_timeout(&kthread->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

       

      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 Threadshttp://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 AhujaLinux Kernel Series: Linux Kernel Threads

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

线程有自己的私有数据:程序计数器,栈空间以及寄存器。

Why Thread?(传统单线程进程的缺点)

1.  现实中有很多需要并发处理的任务,如数据库的服务器端、网络服务器、大容量计算等。

2.  传统的UNIX进程是单线程的,单线程意味着程序必须是顺序执行,不能并发;既在一个时刻只能运行在一个处理器上,因此不能充分利用多处理器框架的计算机。

3.  如果采用多进程的方法,则有如下问题:
a. fork一个子进程的消耗是很大的,fork是一个昂贵的系统调用,即使使用现代的写时复制(copy-on-write)技术。
b. 各个进程拥有自己独立的地址空间,进程间的协作需要复杂的IPC技术,如消息传递和共享内存等。

多线程的优缺点

多线程的优点和缺点实际上是对立统一的。

支持多线程的程序(进程)可以取得真正的并行(parallelism),且由于共享进程的代码和全局数据,故线程间的通信是方便的。它的缺点也是由于线程共享进程的地址空间,因此可能会导致竞争,因此对某一块有多个线程要访问的数据需要一些同步技术。

三种线程——内核线程、轻量级进程、用户线程

内核线程

内核线程就是内核的分身,一个分身可以处理一件特定事情。这在处理异步事件如异步IO时特别有用。内核线程的使用是廉价的,唯一使用的资源就是内核栈和上下文切换时保存寄存器的空间。支持多线程的内核叫做多线程内核(Multi-Threads kernel )。

轻量级进程[*]

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

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

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

LWP.JPG

注:

1.   LWP的术语是借自于SVR4/MP和Solaris 2.x。

2.   有些系统将LWP称为虚拟处理器。

3.   将之称为轻量级进程的原因可能是:在内核线程的支持下,LWP是独立的调度单元,就像普通的进程一样。所以LWP的最大特点还是每个LWP都有一个内核线程支持。

 

用户线程

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

Uthread1.JPG

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

这种模型对应着恐龙书中提到的多对一线程模型,其缺点是一个用户线程如果阻塞在系统调用中,则整个进程都将会阻塞。

加强版的用户线程——用户线程+LWP

这种模型对应着恐龙书中多对多模型。用户线程库还是完全建立在用户空间中,因此用户线程的操作还是很廉价,因此可以建立任意多需要的用户线程。操作系统提供了LWP作为用户线程和内核线程之间的桥梁。LWP还是和前面提到的一样,具有内核线程支持,是内核的调度单元,并且用户线程的系统调用要通过LWP,因此进程中某个用户线程的阻塞不会影响整个进程的执行。用户线程库将建立的用户线程关联到LWP上,LWP与用户线程的数量不一定一致。当内核调度到某个LWP上时,此时与该LWP关联的用户线程就被执行。
Uthread2.JPG

小结:

很多文献中都认为轻量级进程就是线程,实际上这种说法并不完全正确,从前面的分析中可以看到,只有在用户线程完全由轻量级进程构成时,才可以说轻量级进程就是线程。


0 0