Windows程序员如何理解Linux线程

文章介绍了Linux中线程的概念,包括内核线程和轻量级进程(用户级线程),以及线程如何与进程关联。内核线程主要用于执行特定内核函数,而用户级线程在用户空间中实现,由线程库管理。上下文切换是CPU在不同任务间切换的过程,涉及进程上下文、线程上下文和中断上下文。过多的上下文切换会导致性能下降。最后,文章对比了Windows和Linux的线程模型,强调了Linux如何将用户级线程映射到内核级进程上,以及其对系统资源的管理方式。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

我是荔园微风,作为一名在IT界整整25年的老兵,今天我们来看一个问题,就是windows程序员怎么去理解linux线程问题。

我想信,很多windows程序员在刚遇到linux时,都会遇到各种不适应,但有一些你又不得不接触linux,所以要搞懂很多概念和结构。一个很难理解的地方就是linux线程,我们先来看看国外的linux资料里是怎么介绍相关内容的:

上面PPT第一段的文字为:

Kernel threading: Some Unix kernel,such as Solaris and SVR4.2/MP,are organized as a set of kernel threads.A kernel thread is an execution context that can be independently scheduled;it may be associated with a user program,or it may run only some kernel functions.Context switches between kernel threads are usually much less expensive than context switches between ordinary processes,because the former usually operate on a common address space.Linux uses kernel threads in a very limited way to execute a few kernel functions periodically;however,they do not represent the basic execution context abstraction.

翻译过来就是:

内核线程:一些Unix内核,如Solaris和SVR4.2/MP,被组织为一组内核线程。内核线程是一个可以独立调度的执行上下文;它可能与用户程序相关,也可能只运行一些内核函数。内核线程之间的上下文切换通常比普通进程之间的上下文转换成本低得多,因为前者通常在公共地址空间上运行。Linux以非常有限的方式使用内核线程周期性地执行一些内核函数;然而,它们并不表示基本的执行上下文抽象。

上面PPT第二段的文字为:

Multithreaded application support: Most modern operating systems have some kind of support for multithreaded applications—that is,user programs that are designed in terms of many reltively independent execution flows that share a large portion of the application data structures.A multithreaded user application could be composed of many lightweight processes (LWP), which are processes that can operate on a common address space, common physical memory pages, common opened files, and so on. Linux defines its own version of lightweight processes,which is different from the types used on other systems such as SVR4 and Solaris. While all the commercial Unix variants of LWP are based on kernel threads, Linux regards lightweight processes as the basic execution context and handles them via the nonstandard clone() system call.

翻译过来就是:

多线程应用程序支持:大多数现代操作系统都对多线程应用有某种支持,即根据许多相对独立的执行流设计的用户程序,这些执行流共享大部分应用程序数据结构,这些进程可以在公共地址空间、公共物理内存页、公共打开的文件等上运行。Linux定义了自己的轻量级进程版本,这与SVR4和Solaris等其他系统上使用的类型不同。虽然LWP的所有商业Unix变体都基于内核线程,但Linux将轻量级进程视为基本的执行上下文,并通过非标准clone()系统调用来处理它们。

是不是没有看得太明白,这两段是什么关系?为什么上面一段说了线程,下面又跑出来一个多线程,而且里面反复提到执行上下文,执行上下文究竟是什么关系?下面我就一起来搞懂这些。造成大家理解困难的主要原因就是,线程其实有两类:用户级线程和内核级线程。

对于 Linux 来讲,所有的线程都当作进程来实现,因为没有单独为线程定义特定的调度算法,也没有单独为线程定义特定的数据结构(所有的线程或进程的核心数据结构都是 task_struct)。

对于一个进程,相当于是它含有一个线程,就是它自身。对于多线程来说,原本的进程称为主线程,它们在一起组成一个线程组。进程拥有自己的地址空间,所以每个进程都有自己的页表。而线程却没有,只能和其它线程共享某一个地址空间和同一份页表。

这个区别的根本原因是,在进程/线程创建时,因是否拷贝当前进程的地址空间还是共享当前进程的地址空间,而使得指定的参数不同而导致的。具体地说,进程和线程的创建都是执行 clone 系统调用进行的。而 clone 系统调用会执行 do_fork 内核函数,而它则又会调用 copy_process 内核函数来完成。主要包括如下操作:

在调用 copy_process 的过程中,会创建并拷贝当前进程的 task_stuct,同时还会创建属于子进程的 thread_info 结构以及内核栈。此后,会为创建好的 task_stuct 指定一个新的 pid(在 task_struct 结构体中)。然后根据传递给 clone 的参数标志,来选择拷贝还是共享打开的文件,文件系统信息,信号处理函数,进程地址空间等。这就是进程和线程不一样地方的本质所在。每个进程或线程都有只属于自己的 task_struct 对象,是它们各自最为核心的数据结构。

好,讲完linux线程大致情况,我们先来搞懂上下文是什么。

什么是上下文

Linux是一个多任务的操作系统,它支持远大于CPU数量的任务同时运行,当然,这些任务实际上并不是真正的在同时运行,而是系统在很短的时间内,将CPU轮流分配给他们,给用户造成很多任务同时运行的错觉。在每个任务运行前, CPU 都需要知道任务从哪里加载,又从哪里开始运行。也就是说,需要系统事先给他设置好 CPU 寄存器和程序计数器(PC)

CPU 寄存器:是 CPU 内置的容量小、但速度极快的内存

程序计数器:是用来存储 CPU 正在执行的指令位置、或者即将执行的下一条指令位置

综上所述,我们就有答案了

什么是上下文:

我们通常说的上下文又叫CPU上下文,是CPU运行任何任务前,必须依赖的环境,包括CPU 寄存器和程序计数器

上下文切换:就是先把前一个任务的 CPU 上下文(也就是 CPU 寄存器和程序计数器)保存起来,然后加载新任务的上下文到这些寄存器和程序计数器,最后再跳转到程序计数器所指的新位置,运行新任务。

上下文切换详细介绍

根据CPU切换运行任务的不同,又可以分为进程上下文切换、线程上下文切换、中断上下文切换

我们先了解2个上下文切换涉及的知识点,也就是系统调用、进程运行态

系统调用:

从用户态到内核态的转变,需要通过系统调用来完成。比如查看文件时,需要执行多次系统调用:open、read、write、close等。系统调用的过程如下:

把 CPU 寄存器里原来用户态的指令位置保存起来;为了执行内核代码,CPU 寄存器需要更新为内核态指令的新位置,最后跳转到内核态运行内核任务;系统调用结束后,CPU 寄存器需要恢复原来保存的用户态,然后再切换到用户空间,继续运行进程;所以,一次系统调用的过程,其实是发生了两次 CPU 上下文切换。

进程的运行态:

Linux 按照特权等级,把进程的运行空间分为内核空间和用户空间 。在这两种空间中运行的进程状态分别称为内核态和用户态。

内核空间(Ring 0):具有最高权限,可以直接访问所有资源,包括读取文件、分配内存、IO操作、创建子进程……都是内核操作。这也表明,当IO操作频繁时,System参数会很高。

用户空间(Ring 3):只能访问受限资源,不能直接访问内存等硬件设备,必须通过系统调用进入到内核中,才能访问这些特权资源。

典型的用户态空间程序有:Shells、数据库、web服务器、PHP程序、Java程序……在linux系统使用top命令查看cpu时,能看到user和system两项,对应的就是用户态和内核态占用的cpu资源。如上,我们的web服务是运行在用户态下的,对文件的io没有权限,当需要读取文件时,就涉及到系统调用了

进程上下文切换

进程执行终止,它之前控制的CPU就会被释放出来,这时就从就绪队列中取出下一个等待时间片的进程;当某个进程的时间片耗尽,它就会被系统挂起,切换到其他等待CPU的进程运行;某个进程因为需要的系统资源比较大(比如内存不足),这时候该进程会被挂起,系统会调度其他进程执行;当有优先级更高的进程(系统操作进程)需要时间片,为了保证优先级更高的进程能够执行,当前进程会被挂起;如果当前进程中有sleep函数,他也会被挂起。

线程的上下文切换

对操作系统来说,线程是最小的执行单元,进程是最小的资源管理单元。说白了,所谓内核中的任务调用,实际上的调度对象是线程;而进程只是给线程提供了虚拟内存、全局变量等资源。所以,对于现场和进程,我们可以这么理解:

当进程只有一个线程时,可以认为进程就等于线程。当进程拥有多个线程时,这些线程会共享父进程的资源(即共享相同的虚拟内存和全局变量等资源)。这些资源在上下文切换时是不需要修改的。另外,线程也有自己的私有数据,比如栈和寄存器等,这些在上下文切换时也是需要保存的。

综上,线程上下文切换有两种情况:

前后两个线程属于不同进程,因为资源不共享,所以切换过程就跟进程上下文切换是一样的;前后两个线程属于同一个进程,因为虚拟内存是共享的,所以在切换时,虚拟内存这些资源就保持不动,只需要切换线程的私有数据、寄存器等不共享的数据。

中断上下文切换

中断处理会打断进程的正常调度和执行。在打断其他进程时,需要将进程当前的状态保存下来,中断结束后,进程仍然可以从原来的状态恢复运行。

中断上下文切换并不涉及到进程的用户态。所以,即便中断过程打断了一个正处在用户态的进程,也不需要保存和恢复这个进程的虚拟内存、全局变量等用户态资源。中断上下文,其实只包括内核态中断服务程序执行所必须的状态,包括 CPU 寄存器、内核堆栈、硬件中断参数等。

总而言之,每次上下文切换都需要几十纳秒到数微妙的CPU时间,这个时间还是相当可观的。不管是哪种场景导致的上下文切换,你都应该知道:

CPU上下文切换,是保证Linux系统正常工作的核心功能之一,一般情况下不需要我们特别关注。但过多的上下文切换,会把CPU时间消耗在寄存器、内核栈以及虚拟内存等数据的保存和恢复上,从而缩短进程真正运行的时间,导致系统的整体性能大幅下降。

然后,我们再来搞懂用户级线程和内核级线程

用户级线程

用户线程在用户空间中实现,内核并没有直接对用户线程进程调度,内核的调度对象和传统进程一样,是进程(用户进程)本身,内核不能看到用户线程,内核并不知道用户线程的存在。不需要内核支持而在用户程序中实现的线程,其不依赖于操作系统核心,应用进程利用线程库提供创建、同步、调度和管理线程的函数来控制用户线程。内核资源的分配仍然是按照进程(用户进程)进行分配的;各个用户线程只能在进程内进行资源竞争。用户级线程内核的切换由用户态程序自己控制内核切换(通过系统调用来获得内核提供的服务),不需要内核干涉,少了进出内核态的消耗,但不能很好的利用多核CPU。每个用户线程并不具有自身的线程上下文。因此,就线程的同时执行而言,任意给定时刻每个进程只能够有一个线程在运行,而且只有一个处理器内核会被分配给该进程。

在一个纯粹的用户级线程软件中,有关线程管理的所有工作都由应用程序完成,内核意识不到线程的存在。任何应用程序都可以通过使用线程库被设计成多线程程序。线程库是用于用户级线程管理的一个例程包,它包含用于创建和销毁线程的代码、在线程间传递消息和数据的代码、调度线程执行的代码、以及保存和恢复线程上下文的代码。在默认情况下,应用程序从单线程开始,并在该线程中开始运行。该应用程序及其线程被分配给一个由内核管理的进程。在应用程序正在运行(进程处于运行态)的任何时刻,应用程序都可以派生一个在相同进程中运行的新线程(派生线程是通过调用线程库中的派生例程完成的,通过过程调用,控制权被传递给派生例程)。线程库为新线程创建一个数据结构,然后使用某种调度算法,把控制权传递给该进程中处于就绪态的一个线程。当控制权被传递给线程库时,需要保存当前线程的上下文,然后当控制权从线程库中传递给一个线程时,将恢复哪个线程的上下文。上下文实际上包括用户寄存器的内容、程序计数器和栈指针。以上所有活动都发生在用户空间中,并且发生在一个进程内,而内核并不知道这些活动。内核继续以进程为单位进行调度,并且给该进程指定一个执行状态。

优点:

线程的切换无需陷入内核,故切换开销小,速度非常快;由于所有线程管理数据结构都在一个进程的用户地址空间中,线程切换不需要内核态特权;调度可以是应用程序相关的,可以做到为应用程序量身定做调度算法而不扰乱底层的操作系统调度程序;用户级线程可以在任何操作系统中运行,线程库是一组供所有应用程序共享的应用程序级别的函数。

缺点:

系统调用的阻塞问题:对应用程序来讲,同一进程中只能同时有一个线程在运行,一个线程的阻塞将导致整个进程中所有线程的阻塞;由于处理器时间片分配是以进程为基本单位,所以每个线程执行的时间相对减少。在纯粹的用户级线程策略中,多线程应用程序不能利用多处理技术,内核一次只把一个进程分配给一个处理器,因此一次进程中只有一个线程可以执行。

内核级线程

内核线程又称为守护进程,内核线程的调度由内核负责,一个内核线程处于阻塞状态时不影响其他的内核线程,因为其是调度的基本单位。这与用户线程是不一样的;这些线程可以在全系统内进行资源的竞争;内核空间内为每一个内核支持线程设置了一个线程控制块(TCB),内核根据该控制块,感知线程的存在,并进行控制。在一定程度上类似于进程,只是创建、调度的开销要比进程小。内核线程切换由内核控制,当线程进行切换的时候,由用户态转化为内核态。切换完毕要从内核态返回用户态,即存在用户态和内核态之间的转换,比如多核cpu。

优点:

在多处理器系统中,内核能够同时调度同一进程中多个线程并行执行到多个处理器中;如果进程中的一个线程被阻塞,内核可以调度同一个进程中的另一个线程;内核支持线程具有很小的数据结构和堆栈,线程的切换比较快,切换开销小;内核本身也可以使用多线程的方式来实现。

缺点:

即使CPU在同一个进程的多个线程之间切换,也需要陷入内核,因此其速度和效率不如用户级线程。

Windows线程

Windows 使用两类与进程相关的对象:进程和线程。

进程是对应一个拥有内存、打开的文件等资源的用户作业或应用程序的实体。线程是顺序执行的一个科分派的工作单元,并且它是可中断的,因此,处理器可以切换到另一个线程。一个 Windows 进程必须至少包含一个执行线程,该线程可能会创建别的线程。在多处理器系统中,同一个进程的多个线程可以并行的执行。

由于不同进程中的线程可能并非执行,因而 Windows 支持进程间的并发性。此外,同一个进程中的多个线程可以分配给不同的处理器并且同时执行。一个含有多线程的进程在实现并发时,不需要使用多进程的开销。同一个进程中的线程可以通过他们的公共地址空间交换信息,并访问进程中的共享资源,不同进程中的线程可以通过在两个进程间建立的共享内存交换信息

Linux线程

Linux 中的进程或任务由一个 task_struct 数据结构表示。

传统的 UNIX 系统支持每个执行的进程中只有一个单独的一个线程,但现代典型的 UNIX 系统支持一个进程中含有多个 内核级线程。

Linux 提供一种不区分进程和线程的解决方案。用户级线程被映射到内核级进程上,组成一个用户级进程的多个用户级线程被映射到共享同一个组 ID 的多个 Linux 内核级进程上。这使得这些进程可以共享文件和内存等资源,使得同一组中的进程调度切换时不需要切换上下文。

当两个进程共享相同的虚存时,它们可以被当做是一个进程中的线程。

当 Linux 内核执行从一个进程到另一个进程的切换时,它将坚持当前进程的页目录地址是否和将被调度的进程相同。如果相同,那么它们共享同一个地址空间,所以此时上下文切换仅仅是从代码的一处跳转到代码的另一处。虽然属于同一进程组的被克隆的进程共享同一内存空间,但它们不能共享同一个用户栈。

总结

用户级线程对操作系统是未知的,它们由一个在进程的用户空间中运行的线程库创建并管理。用户线程是非常高效的,因为从一个线程切换到另一个线程不需要进行状态切换,但是,一个进程中一次只有一个用户级线程可以执行,如果一个线程发生阻塞,整个进程都会被阻塞。

进程内包含的内核级线程是由内核维护的。由于内核认识它们,因而同一个进程中的多个线程可以再多个处理器上并行执行,一个线程的阻塞不会阻塞整个进程,但当从一个线程切换到另一个线程时就会需要进行模式切换。

作者简介:荔园微风,1981年生,高级工程师,浙大工学硕士,软件工程项目主管,做过程序员、软件设计师、系统架构师,早期的Windows程序员,Visual Studio忠实用户,C/C++使用者,是一位在计算机界学习、拼搏、奋斗了25年的老将,经历了UNIX时代、桌面WIN32时代、Web应用时代、云计算时代、手机安卓时代、大数据时代、ICT时代、AI深度学习时代、智能机器时代,我不知道未来还会有什么时代,只记得这一路走来,充满着艰辛与收获,愿同大家一起走下去,充满希望的走下去。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值