一、操作系统如何为用户提供服务
下面继续了解操作系统(OS),首先需要知道的是在OS不信任任何用户的情况下是如何向用户提供服务的呢?
答:提供系统调用的接口,也叫做OS提供的各种接口,是不是有点类似于银行提供的窗口服务呢?说白了,这些接口其实就是我们平常用的C函数。那么系统调用的接口和平常我们用的各种语言的库函数又有什么关系呢?
系统调用接口 VS C语言库函数 VS C++库函数 VS Jave等等
我们知道操作系统提供的调用接口肯定是要比我们平常使用的各种库函数要复杂一些的。
所以就有人对系统调用的接口进行了软件的封装,以第三方库的形式来进行呈现,例如"printf()",这样就能直接以语言的方式呈现出来。
那么我们从这里就能看出,系统的调用接口与库函数其实是上下层的关系,库函数是上层,调用接口是下层,库函数的调用就是间接使用操作系统的调用接口。
二、进程
在书上是这样对进程进行定义的,“加载到内存的程序就叫进程”。但是又有点废话的意思哈。我们可以这样简单理解:进程=程序文件内容+相关的数据结构,或者可以说:进程=程序+操作系统维护的相关数据结构,这两种理解都是可以的。
那么我们知道,在系统中可以同时存在大量的进程,那么这么多进程操作系统作为一名管理者又是如何进行管理的呢?
答案很简单还是那六个字**“先描述,再组织”**。
那问题又来了,操作系统是如何描述一个进程的呢?这里就要提到一个重要概念,PCB(进程控制块)。
PCB
首先PCB是什么?
在OS上,PCB:进程控制块,就是一个结构体的类型,在Linux中,PCB->struct task_struct{//进程中的所有属性}。
任何进程在形成之前,操作系统都要为该进程创建PCB,struct PCB{…//进程的所有属性}其实就是操作系统对这个进程的描述,Linux操作系统下的PCB是: task_struct。
task_struct中包含了一个进程内部的所有属性信息,那么PCB的内部构成又有什么呢?
struct tast_struct{
1.int pid;//首先肯定是标识符,pid标识符就像身份证标志着进程的唯一性,方便操作系统的查找。
2.int code,exit_code;//状态:任务状态,退出代码,退出信号等例如如果main函数中最后是return 100而不是0,那么此时其他进程是可以读到返回值是100的
3.优先级: 相对于其他进程的优先级,这里需要区别一下优先级和权限,优先级有先后,权限是有无
4.程序计数器: 程序中即将被执行的下一条指令的地址。
5.内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
6.上下文数据: 进程执行时处理器的寄存器中的数据[休学例子,要加图CPU,寄存器]。
7.记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等(说人话就是看你占据使用了CPU多长时间从而让OS较为均衡的调度每个进程)。
8.其他
}
在这里需要说明一下上下文,在说明之前需要先提出一个概念:当系统中有多个进程同时存在时,CPU并不是一个个执行的,而是采用时间片轮转的方式,如果有3个进程同时存在,那么每个进程CPU都会给一个时间片例如10ms,10ms没处理完就会采取轮转的方式处理下一个,被处理的那个就会等待。那么CPU采取时间片轮转的方式就就能给用户更好的体验,就好像有多个进程在同时进行。
我们知道,当CPU处理一个进程时会产生临时数据,但是该进程的处理时间又到了,那么这些临时数据就会再次保存到PCB里面等待下次处理时使用,那么这样就能起到保护上下文和恢复上下文的作用,避免时间到了进程又没处理完导致临时数据丢失的事故。那么通过上下文,我们能感受到进程是被切换的,虽然寄存器硬件只有一份,但是寄存器里面的所有代码都是与进程强相关的,不能随意丢弃。
程序与进程的关系
程序:程序是存在磁盘里面的,程序的本质其实就是文件!比如.exe文件
进程:进程是存储在内存里面的,可执行的文件必须加载到内存中,加上PCB构成了一个进程,其中PCB由操作系统帮我们创建。
曾经我们启动的程序的过程在本质上都是在系统上面创建了一个文件。
在有了PCB后,所有进程管理任务与进程对应的程序都是毫无关系的,与进程对应的PCB强相关,OS不看我们写的各种东西而是直接调用产生的PCB,因为PCB中已经含有程序的各种属性。
fork()函数
在本质上如何理解fork创建的子进程
1.run command和fork:在OS角度,创建进程的方式其实并没有区别。
fork本质上就是创建进程,那么当用fork创建之后系统里面就会多一个进程,既然多了一个进程,那么与进程相关的内核数据结构(tsak_struct)+进程的代码和数据就都会存在。但是我们知识fork了一下,创建了一个子进程,那么这个子进程对应的代码和数据呢?
在默认情况下,子进程会“继承”父进程的代码和数据,内核的数据结构tast_struct也会以父进程为模板,初始化子进程的task_struct,那么fork之后子进程和父进程的代码时共享的,需要注意的是父进程的代码只有一份,而子进程是不可以修改父进程的代码的。
那么对于数据呢?默认情况下,数据也是共享的,不过需要考虑修改的情况。这里就要提到两个概念:
1.进程具有独立性!在系统中当我们打开例如QQ和微信等进程,关闭QQ是对微信的使用是没有影响,那么子进程也不会是依附于父进程的。
2.写时拷贝!子进程要想不依附与父进程,写时拷贝就是一个很重要的办法,当父进程的数据进行修改时,子进程会通过写时拷贝,拷贝下来父进程的数据继续使用,父进程的数据改变不会影响子进程。
那么我们创造一个子进程就是为了和父进程干一样的事情吗?
当然不是的,一般来说,父和子进程干不一样的事,比如在多线程程序中,父进程可能创建多个子线程来执行并行任务,提高程序的运行速度。
当fork的返回值完成时,如果失败一般返回<0,例如返回-1;如果成功,给父进程返回子进程的pid(标识符),给子进程返回0。下面来看一段代码:
#include
2 #include <unistd.h>
3 int main()
4 {
5 pid_t id = fork();
6 if(id == 0)
7 {
8 while(true)
9 {
10 std::cout<<“I am chile.pid:” <<getpid()<<“. ppid:”<<getppid()<<std::endl;
11 sleep(1);
12 }
13 }
14 else if(id>0)
15 {
16 //parent
17 while(true)
18 {
19 std::cout<<“I am parent.pid:” <<getpid()<< “.ppid:” <<getppid()<<std::endl;
20 sleep(2);
21 }
22 }
E> 23 sleep(1);
24 return 0;
25 }
这里看到这段代码的实验结果的if语句和else if语句都执行了,结果是很明显的,出现了两个返回值。
那么怎么理解出现了两个返回值呢?
我们知道当一个函数已经开始了return了,函数的核心功能此时就已经执行完了,有两个返回值就说明有两个进程在发生,而且发生了写时拷贝。
进程状态理解
不同的进程会有不同的状态,那么这些状态信息存放在哪里呢?那就是PCB。
状态的意义:方便OS进行快速判断进程,完成特定的功能,比如调度,其本质就是一种分类的体现。
进程的各种状态:
R运行状态(running): 并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列
里。
S睡眠状态(sleeping): 意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡眠
(interruptible sleep))。
D磁盘休状态(Disk sleep)有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个状态的
进程通会等待IO的结束。
T停止状态(stopped): 可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可
以通过发送 SIGCONT 信号让进程继续运行。
X死亡状态(dead):这个状态只是一个返回状态,你不会在任务列表里看到这个状态。
先来详细了解R(运行状态):处在运行状态的进程一定正在占据CPU吗?不一定!
由于CPU采取的时间片轮转方式,那么会有很多进程处在运行状态,这些进程组成一个队列,轮流使用CPU,处在这个队列run.quequ里面的进程都可以成为R状态。
其次S,D状态:当我们要完成某种任务时,但是任务条件又不具备,需要进程进行某种等待,就会进入S,D状态,并且进入wait.quequ队列里面进行等待,值得注意的是,千万不要以为进程只会等待CPU,也会等待其他需求,如:网卡,磁盘等。其中D就是深度睡眠,OS不可以杀死D进程,除非开机重启。
那么我们可以这样理解:所谓的进程在运行时有可能会因为运行的需要,会在不同的队列里面,那么在不同的队列里,进程的运行的状态就会不同。
把R状态的放到等待wait.quequ里面就是平常说得等待(阻塞),从等待放到run.quequ,被CPU调度就叫唤醒进程。
Z(zombie)-僵尸进程
僵死状态(Zombies)是一个比较特殊的状态。当进程退出并且父进程(使用wait()系统调用,后面讲)
没有读取到子进程退出的返回代码时就会产生僵死(尸)进程
僵死进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码。
所以,只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程进入Z状态。
孤儿进程
简单来说父进程先回收了,此时子进程就会被OS直接接管,这个子进程也叫孤儿进程。
总结
这次将两次课合在了一起写了一篇博客,学习了操作系统的管理,冯.诺依曼结构体系,学习了有关进程的各种知识,了解了fork函数本次博客就到这里。
2023年6月4日