一,操作系统接口interface
操作系统向用户提供了“用户与操作系统的接口”,以支持用户和操作系统之间进行交互。用户与操作系统的接口通常是由“命令”和“系统调用”的形式表现出来的。接口起到连接两个层次,实现信号转换,屏蔽信息细节的作用。
如此一来便实现了操作系统的使用,这些重要的函数由操作系统提供,接口表现为函数调用,又称为系统调用。系统调用就是操作系统接口。比如以下函数都是操作系统接口:
操作系统的接口函数有很多,没必要都记住,但是要学会怎么去查找。
遇到需要就去查看POSIX手册即可。官网:https://pubs.opengroup.org/onlinepubs/7908799/index.html
二,内核态和用户态
计算机提供了一种硬件机制,将内存分为两段不同的区域——内核段(即计算机加载完操作系统之后将操作系统从硬盘中转移到内存0地址开始的那一段)和用户段,内核态的数据在这时DPL为0,若当前的调用CPL<DPL,才可以成功调取内核段中的数据。这就阻止了用户程序胡乱调用操作系统中十分重要的数据,因为用户程序使用调用机制时CPL为3,是不符合CPL<DPL的。
CPL是当前进程的权限级别(Current Privilege Level),是当前正在执行的代码所在的段的特权级,存在于cs寄存器的低两位。
RPL说明的是进程对段访问的请求权限(Request Privilege Level),是对于段选择子而言的,每个段选择子有自己的RPL,它说明的是进程对段访问的请求权限,有点像函数参数。而且RPL对每个段来说不是固定的,两次访问同一段时的RPL可以不同。RPL可能会削弱CPL的作用,例如当前CPL=0的进程要访问一个数据段,它把段选择符中的RPL设为3,这样虽然它对该段仍然只有特权为3的访问权限。
DPL存储在段描述符中,规定访问该段的权限级别(Descriptor Privilege Level),每个段的DPL固定。当进程访问一个段时,需要进程特权级检查,一般要求DPL >= max {CPL, RPL}
三,中断是进入内核的唯一方法
那么如果用户程序是如何通过系统调用实现用户段和内核段之间的联系的呢?
以用户态的printf为例,首先由库函数将printf转换为带有中断指令的代码。调用中断时,将DPL变为3,进入内核态之后,CPL变成了0,这时CPL<DPL,便调用system_call,由system_call调用call_table去查表,查到相对应的系统调用号,根据这个调用号才去执行真正要去执行相对应的系统调用,pringtf为例是由调用号4最终去调用了sys_write。
四,操作系统核心的两点在于多进程管理和文件管理
1)多进程管理
如果在cpu在工作时单道程序执行,由于外设的速度远远低于cpu的工作速度,这样会使cpu的效率大大降低,使其在大部分是时间都在等外外设准备好文件数据。这里参考程序查询方式。如果在一道程序之中外设还没准备好的时候cpu切换出去执行其他的程序,然后在切换出去执行的这一道程序又出现要等待外设准备数据的情况时再进行cpu的工作切换。这样就实现了多道程序交替执行,大大提高了cpu的工作效率。
像这样一个CPU上交替执行多个程序称为并发。那么怎么做到并发呢?由于cpu是取指执行的,而这一行动靠的是PC指针,由它从内存中取指令和数据。所以并发的关键是“切换PC指针从而跳转执行其它程序。”。但是由于只是暂时跳开执行其它的程序而提高cpu效率,而不是不再执行原有的程序,所以要保护现场,也就是要能够保留切换出去之前的程序原来的样子,包括pc和相应的寄存器。完成“保护现场”这一工作的是进程控制块PCB(process control block),以记录每一个运行中的程序的状态。
PCB:为了描述控制进程的运行,系统中存放进程的管理和控制信息的数据结构称为进程控制块PCB (Process Control Block),它是进程实体的一部分,是操作系统中最重要的记录性数据结构。它是进程管理和控制的最重要的数据结构,每一个进程均有一个PCB,在创建进程时,建立PCB,伴随进程运行的全过程,直到进程撤消而撤消。
操作系统的开启初始化过程中执行到main程序时有一段fork()指令如下,完成了桌面的启动:
所以main是0号进程,init是1号进程。
1号进程init()一直在执行。
fork():在fork函数执行完毕后,如果创建新进程失败,则返回-1;如果创建新进程成功,则出现两个进程,一个是子进程,一个是父进程。在子进程中,fork函数返回0,在父进程中,fork返回新创建子进程的进程ID。我们可以通过fork返回的值来判断当前进程是子进程还是父进程。
进程的状态有五种:
正在运行的程序遇到突发事件比如错误,或者需要等待外设时就要切换当前进程到阻塞态,然后调用schedule()函数完成进程替代。Schedule()函数最主要作用就是从就绪进程中选择一个优先级最高的进程来代替当前进程运行。
getnext()是调度函数,涉及到优先级的设计,比如FIFO策略,除此之外还有很多的复杂算法。
比如:pCur进程需要等待外设准备数据,就将其转换成等待状态,然后调用schedule()。
2)文件管理
五,线程
早期时候进程是能拥有资源和独立运行的最小单位,也是程序执行的最小单位。任务调度采用的是时间片轮转的抢占式调度方式,而进程是任务调度的最小单位,每个进程有各自独立的一块内存,通过映射表将虚拟内存转换到相应的物理地址,使得各个进程之间内存地址相互隔离。但是进程之间的切换开销较大(要切换指令,还要切换页表),已经无法满足越来越复杂的程序的要求了。而后来线程成了程序执行中程序执行流的最小单元,是处理器调度和分派的基本单位。一个进程可以有一个或多个线程,各个线程之间共享程序的内存空间(也就是所在进程的内存空间,所以线程切换就不用切换页表,开销较小)。一个标准的线程由线程ID、当前指令指针(PC)、寄存器和堆栈组成。而进程由内存空间(代码、数据、进程空间、打开的文件)和一个或多个线程组成。
线程:在一个进程中,当一个线程任务执行几毫秒后,会由操作系统的内核(负责管理各个任务)进行调度,通过硬件的计数器中断处理器,让该线程强制暂停并将该线程的寄存器放入内存中,通过查看线程列表决定接下来执行哪一个线程,并从内存中恢复该线程的寄存器,最后恢复该线程的执行,从而去执行下一个任务。线程的栈和pc寄存器不共享(否则会发生线程切换出错,参考:https://blog.csdn.net/ztliduo/article/details/54565504)。
概念小结:
进程是并发执行的程序在执行过程中分配和管理资源的基本单位,是一个动态概念,竞争计算机系统资源的基本单位。
线程是进程的一个执行单元,是进程内科调度实体。比进程更小的独立运行的基本单位。线程也被称为轻量级进程。
因为进程之间是有自己的存储块的,如果每个执行单元都是进程,那么相互之间如果涉及到数据调用时,还得先从一个进程的存储空间将数据拷贝到另一个进程的存储空间,这样就耗费了大量的不必要开销,为什么说不必要呢?因为如果有那么两个特殊的“进程”,共用一块存储空间(还有别的模块),那么就可以达到提高效率的作用,这就是所谓的线程。
举个例子说明,浏览器打开一个网址。
任务一:要从服务器下载资源。
任务二:显示文本
任务三:显示图像
上面三个任务如果分别单独成进程,那么每次完成任务二三都要先从做任务一用作存储的空间中提取数据放入自己的储存区(这就是进程上下文切换的一部分),之后再进行任务二三。
Pthread_create就是建立线程,那么CPU是如何切换线程的呢?(在函数之间增加函数调用)
(通过yeild函数进行线程切换)
yield函数:yield的意思是“屈服、礼让”,在程序中表现为当前线程会尽量让出CPU资源来给其他线程执行。
线程切换的核心就是完成cpu指针在不同线程的栈指针中的转换,比如AB线程分别的栈为栈A和栈B,他们的栈指针esp分别为1000和2000,那么yeild完成的就是切换espA和espB赋值cpu的esp寄存器。
(完成的是从线程2切换回线程1)
(参考:https://cloud.tencent.com/developer/article/1462439)
进程与线程两者区别:
系统开销:在创建或撤消进程时,由于系统都要为之分配和回收资源,导致系统的开销明显大于创建或撤消线程时的开销。但是进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不同执行路径。线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,一个进程死掉就等于所有的线程死掉,所以多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些。
地址空间:线程共享本进程的地址空间,而进程之间是独立的地址空间。
资源:线程共享本进程的资源如内存、I/O、cpu等,不利于资源的管理和保护,而进程之间的资源是独立的,能很好的进行资源管理和保护。
健壮性:多进程要比多线程健壮,一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉。
执行过程:每个独立的进程有一个程序运行的入口、顺序执行序列和程序入口,执行开销大。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制,执行开销小。
可并发性:两者均可并发执行。
切换时:进程切换时,消耗的资源大,效率高。所以涉及到频繁的切换时,使用线程要好于进程。同样如果要求同时进行并且又要共享某些变量的并发操作,只用线程不能用进程。
其他:线程是处理器调度的基本单位,但是进程不是。
1)用户态线程(即协程):协程是一种用户态线程不用进入内核。它比线程更加轻量,并且协程对于操作系统是并不可见的,其实就是说OS感知不到用户级线程,OS感知的是内核级线程。同一时刻一个CPU只会执行一个协程。
协程如果遇到阻塞(比如网络卡顿),会schedule切换到其他的进程,那么之前的那个进程中的线程不执行,需要等待切换回来这个进程才能继续执行下面的线程。
如果是核心线程,并发性就会更好一些,线程不会因为Schedule切换而导致滞停。
2)内核线程:(https://blog.csdn.net/qq_42518941/article/details/119145575)
线程S和线程T:从线程S的用户栈到内核栈,再到S的TCB,完成S和T的TCB切换,接着进入T的内核栈,再弹到T的用户栈。