系统调用
os提供了很多底层的重要函数,我们的程序调用这些底层的函数,被称为系统调用,举例:
为了保证安全性,我们并不能随意访问系统的资源,系统函数就是为用户提供的一种访问系统资源的安全的方式。
内核态和用户态
os通过划分内核态和用户态来达到这一隔离的目的,内核态可以访问任何数据,用户态不能访问内核数据。如Linux输入一个whoami命令,其实这个whoami就是一个系统函数,我们只是通过用户态去系统调用内核态的函数。
内核态可以访问任何数据,用户态不能访问内核数据,这一分层都是依靠硬件实现的,虽然都是在内存中但是把内存划分成了核心段和用户段,不能随意访问。
用户发起系统调用内核的方法的唯一方式就是中断指令int,达到安全性的目的。
其中用户态的whoami方法中包含了中断指令int的代码,将进入内核态调用内核态的whoami方法
进程
为了提高cpu的利用率,不至于一直死等io操作(IO操作比计算操作要耗费的时间多得多),一个cpu应该交替的执行多个程序,称为并发。这些执行的程序被称为进程。cpu并发的执行不同的进程,需要进行上下文切换,记录进程的状态。操作系统依赖PCB(进程控制块)感知线程。
如打开cmd,初始化只有一个shell进程,当我们输入ifconfig命令时,系统就会执行fork打开一个新进程去执行该命令。
多进程的组织
操作系统就是以这样的形式对多进程进行管理控制的。
进程间的切换是使用os中schedule()这个函数,schedule()中通过查询就绪态选择进程进行调度,具体依赖调度算法选择合适的一个,然后进行上下文切换。
每个进程都有它的内存映射表,使得进程之间的数据隔离。当切换进程时也需要切换对应的内存映射表。
线程
在一个进程之中分出多个指令执行序列、但这些指令序列又共享一个进程中的资源,这些指令序列称为线程,线程间切换代价比较小,不需要切换映射表,只是指令间的切换即可,比较轻量级。
线程(thread):保留了并发的优点,避免了进程切换的代价。实质就是映射表不变而PC指针变
线程间的切换是依靠TCB(线程控制块)实现的
用户级线程
如:网页浏览器访问一个网站时,一开始在浏览器这个进程里create多个线程,一个线程用来接收服务器的数据,一个线程用于显示文本,一个线程处理图片,一个线程用来显示图片,这几个线程互相切换的执行。当接收数据的线程接收到数据后可以调用yield函数释放cpu的使用权给到其他的线程并发的执行多个线程……
这种方式主要是使用yield实现了多线程的并发运行,create和yield都是用户程序(yield的核心代码就是在多线程间跳转),执行的时候不需要访问内核,所以使用这种方法实现的多线程叫做用户级线程。
用户级线程内核是感受不到的,这就导致了如果一个进程中的其中一个线程阻塞了,该进程也会被阻塞(包括进程中的其他线程)。
如上图:当进程一中的接收数据的线程执行时需要进行读写网卡的io操作了,相应的进程一就会进入内核态(因为要访问硬件了),此时io操作花费的时间会比较长,所以进程一进入到阻塞态等待io操作完成,此时cpu(cpu感知不到进程一还有其他线程要执行)就会去执行进程二,所以进程一中的其他线程本来是可以继续执行的,但是因为一个线程而导致了进程被阻塞而得不到执行。所以一些浏览器已经从多线程改为了多进程的方式,每打开一个标签就是一个进程。
计算密集型的,用户级线程就可以,因为不需要IO操作
核心级线程(Kernel Threads)
核心级线程的线程创建(thread_create)是一个系统调用,创建线程会进如到内核,TCB在内核中。当线程进行切换的时候,由用户态转化为内核态,切换完毕要从内核态返回用户态。
如上图:当进程一的某个线程阻塞了,cpu知道进程一还有其他线程,并不会阻塞进程,其他线程也会有机会得到运行,也会被调度,这种内核级线程的调度叫做schedule而不叫yield。
内核级线程的并发性要更好一些。
多核cpu不像多处理cpu一样有多个内存映射表(MMU),它只有一个,多个执行序列用共同一个映射表,这其实就是线程。我们再看看核心级线程图,如果想让多个线程分配到多个cpu核心上,就需要使用核心级线程,才能充分的使用到多核心cpu,因为用户级线程cpu没法感知到,没法分配硬件给它,用户级线程只能以进程的方式得到利用。
总结
区别:
- 内核支持线程是OS内核可感知的,而用户级线程是OS内核不可感知的。
- 用户级线程的创建、撤消和调度不需要OS内核的支持,是在语言(如Java)这一级处理的;而内核支持线程的创建、撤消和调度都需OS内核提供支持,而且与进程的创建、撤消和调度大体是相同的。
- 用户级线程执行系统调用指令时将导致其所属进程被中断,而内核支持线程执行系统调用指令时,只导致该线程被中断。
- 在只有用户级线程的系统内,CPU调度还是以进程为单位,处于运行状态的进程中的多个线程,由用户程序控制线程的轮换运行;在有内核支持线程的系统内,CPU调度则以线程为单位,由OS的线程调度程序负责线程的调度。
- 用户级线程的程序实体是运行在用户态下的程序,而内核支持线程的程序实体则是可以运行在任何状态下的程序。
内核线程的优点:
- 当有多个处理器时,一个进程的多个线程可以同时执行。
- 多处理器系统中,内核能够并行执行同一进程中的多个线程。
缺点:
- 需要内核调度,增加了用户态和核心态切换的消耗
用户线程的优点:
- 线程的调度不需要内核直接参与,控制简单。
- 可以在不支持线程的操作系统中实现。
- 创建和销毁线程、线程切换代价等线程管理的代价比内核线程少得多。
- 允许每个进程定制自己的调度算法,线程管理比较灵活。
- 线程能够利用的表空间和堆栈空间比内核级线程多。
缺点:
- 资源调度按照进程进行,多个处理机下,同一个进程中的线程只能在同一个处理机下分时复用
- 同一进程中只能同时有一个线程在运行,如果有一个线程使用了系统调用而阻塞,那么整个进程都会被挂起。另外,页面失效也会产生同样的问题。
轻量级线程
轻量级进程(LWP)是建立在内核之上并由内核支持的用户线程,它是内核线程的高度抽象,每一个轻量级进程都与一个特定的内核线程关联。内核线程只能由内核管理并像普通进程一样被调度。
可以看到图中:轻量级线程可以由用户管理,但是每一个线程又需要一个内核支持,所以轻量级线程就是对内核级线程的一种优化。
由于每个LWP都与一个特定的内核线程关联,因此每个LWP都是一个独立的线程调度单元。即使有一个LWP在系统调用中阻塞,也不会影响整个进程的执行。
轻量级进程具有局限性。
-
首先,大多数LWP的操作,如建立、析构以及同步,都需要进行系统调用。系统调用的代价相对较高:需要在user mode和kernel mode中切换。
-
其次,每个LWP都需要有一个内核线程支持,因此LWP要消耗内核资源(内核线程的栈空间)。因此一个系统不能支持大量的LWP。
- LWP的术语是借自于SVR4/MP和Solaris 2.x。
- 有些系统将LWP称为虚拟处理器。
- 将之称为轻量级进程的原因可能是:在内核线程的支持下,LWP是独立的调度单元,就像普通的进程一样。所以LWP的最大特点还是每个LWP都有一个内核线程支持。
协程
协程运行在线程之上,当一个协程执行完成后,可以选择主动让出,让另一个协程运行在当前线程之上。协程并没有增加线程数量,只是在线程的基础之上通过分时复用的方式运行多个协程,而且协程的切换在用户态完成,切换代价很小,从广义上分的话可以说携程就是用户级线程,不用内核支持。
携程就是程序员自己写的,自己在内存中维护一个堆栈,它能够在自己的程序中实现多线程并发的运行,即使你的os不支持多线程,你也可以使用携程模拟多线程。
在有大量IO操作业务的情况下,我们采用协程替换线程,可以到达很好的效果,一是降低了系统内存,二是减少了系统切换开销,因此系统的性能也会提升。
在协程中尽量不要调用阻塞IO的方法,比如打印,读取文件,Socket接口等,除非改为异步调用的方式,并且协程只有在IO密集型的任务中才会发挥作用。
协程只有和异步IO结合起来才能发挥出最大的威力。
现如今主流的多线程闻名的语言如golang,erlang都是采用的携程。