1-线程的三种模型
2-内核线程、轻量级进程、用户线程基本概念
3-linux进程的创建流程
总结
1-线程的三种模型
1.1-用户级线程(多对一模型)
库调度器从进程的多个线程中选择一个线程,然后该线程和该进程允许的一个内核线程关联起来。内核线程将被操作系统调度器指派到处理器内核。用户级线程是一种”多对一”的线程映射
- 特点:内核对线程包一无所知。从内核角度考虑,就是按正常的方式管理,即单线程进程(存在运行时系统)
- 优点:不需要陷阱,不需要上下文切换,也不需要对内存高速缓存进行刷新,使得线程调用非常快捷
- 缺点:线程发生I/O或页面故障引起的阻塞时,如果调用阻塞系统调用则内核由于不知道有多线程的存在,而会阻塞整个进程从而阻塞所有线程, 因此同一进程中只能同时有一个线程在运行
1.2-内核线程(一对一模型)
内核线程驻留在内核空间,它们是内核对象。有了内核线程,每个用户线程被映射或绑定到一个内核线程。用户线程在其生命期内都会绑定到该内核线程。一旦用户线程终止,两个线程都将离开系统。这被称作”一对一”线程映射,
如果进程中的一个线程被阻塞,能够切换同一进程内的其他线程继续执行(用户级线程的一个缺点)
1.3-混合线程模型
在一些系统中,使用组合方式的多线程实现, 线程创建完全在用户空间中完成,线程的调度和同步也在应用程序中进行. 一个应用程序中的多个用户级线程被映射到一些(小于或等于用户级线程的数目)内核级线程上。
pthread_attr_t attr;
pthread_attr_init(&attr);
pthread_attr_setscope(&attr, PTHREAD_SCOPE_SYSTEM); //设置内核级的线程,以获取较高的响应速度
//创建线程
ret = pthread_create(&iAcceptThreadId, &attr, AcceptThread, NULL);
PTHREAD_SCOPE_SYSTEM:设置内核级的线程(linux 只有这种,绑定的)
PTHREAD_SCOPE_PROCESS:用户级别线程(linux 失效,非绑定的)
2-内核线程、轻量级进程、用户线程基本概念
内核线程
内核线程就是内核的分身,一个分身可以处理一件特定事情。这在处理异步事件如异步IO时特别有用。内核线程的使用是廉价的,唯一使用的资源就是内核栈和上下文切换时保存寄存器的空间。支持多线程的内核叫做多线程内核(Multi-Threads kernel )。
内核线程只运行在内核态,不受用户态上下文的拖累。
- 处理器竞争:可以在全系统范围内竞争处理器资源;
- 使用资源:唯一使用的资源是内核栈和上下文切换时保持寄存器的空间
- 调度:调度的开销可能和进程自身差不多昂贵
- 同步效率:资源的同步和数据共享比整个进程的数据同步和共享要低一些。
轻量级进程
轻量级进程(LWP)是建立在内核之上并由内核支持的用户线程,它是内核线程的高度抽象,每一个轻量级进程都与一个特定的内核线程关联。内核线程只能由内核管理并像普通进程一样被调度
轻量级进程由clone()系统调用创建,参数是CLONE_VM,即与父进程是共享进程地址空间和系统资源。
与普通进程区别:LWP只有一个最小的执行上下文和调度程序所需的统计信息。
- 处理器竞争:因与特定内核线程关联,因此可以在全系统范围内竞争处理器资源
- 使用资源:与父进程共享进程地址空间
-
调度:像普通进程一样调度
用户线程(linux不是这种实现方式。 java 是的)
用户线程是完全建立在用户空间的线程库,用户线程的创建、调度、同步和销毁全又库函数在用户空间完成,不需要内核的帮助。因此这种线程是极其低消耗和高效的。
- 处理器竞争:单纯的用户线程是建立在用户空间,其对内核是透明的,因此其所属进程单独参与处理器的竞争,而进程的所有线程参与竞争该进程的资源。
- 使用资源:与所属进程共享进程地址空间和系统资源。
- 调度:由在用户空间实现的线程库,在所属进程内进行调度
加强版的用户线程——用户线程+LWP, 这个不做介绍了
综述:
LinuxThreads是用户空间的线程库,所采用的是线程-进程1对1模型(即一个用户线程对应一个轻量级进程,而一个轻量级进程对应一个特定的内核线程)
LinuxThreads只支持调度范围为PTHREAD_SCOPE_SYSTEM的调度,默认的调度策略是SCHED_OTHER。
- SCHED_OTHER 分时调度策略,
- SCHED_FIFO 实时调度策略,先到先服务
- SCHED_RR 实时调度策略,时间片轮转
很多文献中都认为轻量级进程就是线程,实际上这种说法并不完全正确,从前面的分析中可以看到,只有在用户线程完全由轻量级进程构成时,才可以说轻量级进程就是线程。
3-linux进程的创建流程
进程的复制fork和加载execve
新的进程是通过fork和execve创建的,首先通过fork从父进程分叉出一个基本一致的副本,然后通过execve来加载新的应用程序镜像
fork生成当前进程的的一个相同副本,该副本成为子进程
原进程(父进程)的所有资源都以适当的方法复制给新的进程(子进程)。因此该系统调用之后,原来的进程就有了两个独立的实例,这两个实例的联系包括:同一组打开文件, 同样的工作目录, 进程虚拟空间(内存)中同样的数据(当然两个进程各有一份副本, 也就是说他们的虚拟地址相同, 但是所对应的物理地址不同)等等。
execve从一个可执行的二进制程序镜像加载应用程序, 来代替当前运行的进程
换句话说, 加载了一个新的应用程序。因此execv并不是创建新进程
写时复制技术
写入时复制(Copy-on-write)是一个被使用在程式设计领域的最佳化策略。其基础的观念是,如果有多个呼叫者(callers)同时要求相同资源,他们会共同取得相同的指标指向相同的资源,直到某个呼叫者(caller)尝试修改资源时,系统才会真正复制一个副本(private copy)给该呼叫者,以避免被修改的资源被直接察觉到,这过程对其他的呼叫只都是通透的(transparently)。此作法主要的优点是如果呼叫者并没有修改该资源,就不会有副本(private copy)被建立。
Linux内核采用一种更为有效的方法,称之为写时复制(Copy On Write,COW)。这种思想相当简单:父进程和子进程共享页帧而不是复制页帧。然而,只要页帧被共享,它们就不能被修改,即页帧被保护。无论父进程还是子进程何时试图写一个共享的页帧,就产生一个异常,这时内核就把这个页复制到一个新的页帧中并标记为可写。原来的页帧仍然是写保护的:当其他进程试图写入时,内核检查写进程是否是这个页帧的唯一属主,如果是,就把这个页帧标记为对这个进程是可写的。
当进程A使用系统调用fork创建一个子进程B时,由于子进程B实际上是父进程A的一个拷贝,
因此会拥有与父进程相同的物理页面.为了节约内存和加快创建速度的目标,fork()函数会让子进程B以只读方式共享父进程A的物理页面.同时将父进程A对这些物理页面的访问权限也设成只读.
这样,当父进程A或子进程B任何一方对这些已共享的物理页面执行写操作时,都会产生页面出错异常(page_fault int14)中断,此时CPU会执行系统提供的异常处理函数do_wp_page()来解决这个异常.
do_wp_page()会对这块导致写入异常中断的物理页面进行取消共享操作,为写进程复制一新的物理页面,使父进程A和子进程B各自拥有一块内容相同的物理页面.最后,从异常处理函数中返回时,CPU就会重新执行刚才导致异常的写入操作指令,使进程继续执行下去.
一个进程调用fork()函数后,系统先给新的进程分配资源,例如存储数据和代码的空间。然后把原来的进程的所有值都复制到新的新进程中,只有少数值与原来的进程的值(比如PID)不同。相当于克隆了一个自己。
Linux下线程的实现机制
Linux实现线程的机制非常独特。从内核的角度来说, 他并没有线程这个概念。Linux把所有的进程都当做进程来实现。内核中并没有准备特别的调度算法或者定义特别的数据结构来表示线程。相反, 线程仅仅被视为一个与其他进程共享某些资源的进程。每个线程都拥有唯一隶属于自己的task_struct, 所以在内核看来, 它看起来就像式一个普通的进程(只是线程和同组的其他进程共享某些资源)
getpid()返回当前进程的进程号,返回的应该是tgid值而不是pid的值, 对于用户空间来说同组的线程拥有相同进程号即tgid, 而对于内核来说, 某种成都上来说不存在线程的概念, 那么pid就是内核唯一区分每个进程的标示。
- linux下进程或者线程的创建开销很小
- 既然不管是线程或者进程内核都是不加区分的,一组共享地址空间或者资源的线程可以组成一个线程组, 那么其他进程即使不共享资源也可以组成进程组, 甚至来说一组进程组也可以组成会话组, 进程组可以简化向所有组内进程发送信号的操作, 一组会话也更能适应多道程序环境
内核线程
内核线程的使用是廉价的,唯一使用的资源就是内核栈和上下文切换时保存寄存器的空间。支持多线程的内核叫做多线程内核(Multi-Threads kernel )。内核线程的调度由内核负责,一个内核线程处于阻塞状态时不影响其他的内核线程,因为其是调度的基本单位。
内核线程与普通进程的异同
- 跟普通进程一样,内核线程也有优先级和被调度。
- 内核线程的bug直接影响内核,很容易搞死整个系统, 但是用户进程处在内核的管理下,其bug最严重的情况也只会把自己整崩溃
- 内核线程没有自己的地址空间,所以它们的”current->mm”都是空的;
- 内核线程只能在内核空间操作,不能与用户空间交互;
内核会将其task_strcut的active_mm指向前一个被调度出的进程的mm域, 在需要的时候,内核线程可以使用前一个进程的内存描述符。
因为内核线程不访问用户空间,只操作内核空间内存,而所有进程的内核空间都是一样的。这样就省下了一个mm域的内存。
内核线程创建
kernel_thread
int kernel_thread(int (*fn)(void *), void *arg, unsigned long flags);
// fn为线程函数,arg为线程函数参数,flags为标记
void daemonize(const char * name,...); // name为内核线程的名称
kernel_thread接口,使用该接口创建的线程,必须在该线程中调用daemonize()函数,这是因为只有当线程的父进程指向”Kthreadd”(2号进程)时,该线程才算是内核线程,而恰好daemonize()函数主要工作便是将该线程的父进程改成“kthreadd”内核线程;默认情况下,调用deamonize()后,会阻塞所有信号,如果想操作某个信号可以调用allow_signal()函数。
kthread_create
struct task_struct *kthread_create(int (*threadfn)(void *data),void *data,
const char namefmt[], ...);
//threadfn为线程函数;data为线程函数参数;namefmt为线程名称,可被格式化的
线程创建后,不会马上运行,而是需要将kthread_create() 返回的task_struct指针传给wake_up_process(),然后通过此函数运行线程。
kthread_run
struct task_struct *kthread_run(int (*threadfn)(void *data),
void *data,
const char *namefmt, ...);
线程一旦启动起来后,会一直运行,除非该线程主动调用do_exit函数,或者其他的进程调用kthread_stop函数,结束线程的运行。
内核中线程也是进程,所以其结构体也是使用进程的结构体”struct task_struct”。
总结
Linux使用task_struct来描述进程和线程
- 一个进程由于其运行空间的不同, 从而有内核线程和用户进程的区分, 内核线程运行在内核空间, 之所以称之为线程是因为它没有虚拟地址空间, 只能访问内核的代码和数据, 而用户进程则运行在用户空间, 不能直接访问内核的数据但是可以通过中断, 系统调用等方式从用户态陷入内核态,但是内核态只是进程的一种状态, 与内核线程有本质区别
- 用户进程运行在用户空间上, 而一些通过共享资源实现的一组进程我们称之为线程组, Linux下内核其实本质上没有线程的概念, Linux下线程其实上是与其他进程共享某些资源的进程而已。但是我们习惯上还是称他们为线程或者轻量级进程
因此, Linux上进程分3种,内核线程、用户进程、用户线程.你也可以认为用户进程和用户线程都是用户进程。
- 内核线程拥有 进程描述符、PID、进程正文段、核心堆栈
- 用户进程拥有 进程描述符、PID、进程正文段、核心堆栈 、用户空间的数据段和堆栈
- 用户线程拥有 进程描述符、PID、进程正文段、核心堆栈,同父进程共享用户空间的数据段和堆栈
`用户线程也可以通过exec函数族拥有自己的用户空间的数据段和堆栈,成为用户进程