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 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+v2.6.19/arch/arm/kernel/process.c
460 pid_t kernel_thread(int (*fn)(void *), void *arg, unsigned long flags)
461{
464 memset(®s, 0, sizeof(regs));
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;
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 <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.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.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 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"); ========================================================================
1 .内核级线程:切换由内核控制,当线程进行切换的时候,由用户态转化为内核态。切换完毕要从内核态返回用户态;可以很好的利用smp,即利用多核cpu。windows线程与Linux pthread大体是这么做的。 2. 用户级线程内核的切换由用户态程序自己控制内核切换,不需要内核干涉,少了进出内核态的消耗,但不能很好的利用多核Cpu, 线程的实现可以分为两类:用户级线程(User-Level Thread)和内核线线程(Kernel-Level Thread),后者又称为内核支持的线程或轻量级进程。在多线程操作系统中,各个系统的实现方式并不相同,在有的系统中实现了用户级线程,有的系统中实现了内核级线程。 用户线程指不需要内核支持而在用户程序中实现的线程,其不依赖于操作系统核心,应用进程利用线程库提供创建、同步、调度和管理线程的函数来控制用户线程。不需要用户态/核心态切换,速度快,操作系统内核不知道多线程的存在,因此一个线程阻塞将使得整个进程(包括它的所有线程)阻塞。由于这里的处理器时间片分配是以进程为基本单位,所以每个线程执行的时间相对减少。 内核线程:由操作系统内核创建和撤销。内核维护进程及线程的上下文信息以及线程切换。一个内核线程由于I/O操作而阻塞,不会影响其它线程的运行。Windows NT和2000/XP支持内核线程。 用户线程运行在一个中间系统上面。目前中间系统实现的方式有两种,即运行时系统(Runtime System)和内核控制线程。“运行时系统”实质上是用于管理和控制线程的函数集合,包括创建、撤销、线程的同步和通信的函数以及调度的函数。这些函数都驻留在用户空间作为用户线程和内核之间的接口。用户线程不能使用系统调用,而是当线程需要系统资源时,将请求传送给运行时,由后者通过相应的系统调用来获取系统资源。内核控制线程:系统在分给进程几个轻型进程(LWP),LWP可以通过系统调用来获得内核提供的服务,而进程中的用户线程可通过复用来关联到LWP,从而得到内核的服务。 以下是用户级线程和内核级线程的区别: (1)内核支持线程是OS内核可感知的,而用户级线程是OS内核不可感知的。 (2)用户级线程的创建、撤消和调度不需要OS内核的支持,是在语言(如Java)这一级处理的;而内核支持线程的创建、撤消和调度都需OS内核提供支持,而且与进程的创建、撤消和调度大体是相同的。 (3)用户级线程执行系统调用指令时将导致其所属进程被中断,而内核支持线程执行系统调用指令时,只导致该线程被中断。 (4)在只有用户级线程的系统内,CPU调度还是以进程为单位,处于运行状态的进程中的多个线程,由用户程序控制线程的轮换运行;在有内核支持线程的系统内,CPU调度则以线程为单位,由OS的线程调度程序负责线程的调度。 (5)用户级线程的程序实体是运行在用户态下的程序,而内核支持线程的程序实体则是可以运行在任何状态下的程序。 内核线程的优点: (1)当有多个处理机时,一个进程的多个线程可以同时执行。 缺点: (1)由内核进行调度。 用户进程的优点: (1) 线程的调度不需要内核直接参与,控制简单。 (2) 可以在不支持线程的操作系统中实现。 (3) 创建和销毁线程、线程切换代价等线程管理的代价比内核线程少得多。 (4) 允许每个进程定制自己的调度算法,线程管理比较灵活。 (5) 线程能够利用的表空间和堆栈空间比内核级线程多。 (6) 同一进程中只能同时有一个线程在运行,如果有一个线程使用了系统调用而阻塞,那么整个进程都会被挂起。另外,页面失效也会产生同样的问题。 缺点: (1)资源调度按照进程进行,多个处理机下,同一个进程中的线程只能在同一个处理机下分时复用 |
更多推荐
所有评论(0)