1.操作系统(OS)
简单来说, 操作系统就是一款进行软硬件资源管理的软件
管理的实例
在一个公司系统中, 毫无无疑问公司是由公司老总来管理的, 普通员工就是被管理的对象。但是我们似乎从来没有直接的跟老总打过交道,那他是怎么管理的我们呢,当然是通过经理来拿到我们的信息,然后把这些信息汇总给老总。因此老总管理员工是通过员工数据来实现的,在这过程中,提供员工数据的是经理。
我们从上面的例子可以得出几个结论
1.管理:管理者和被管理者,其实是不需要直接沟通的
2.管理的本质:是对被管理对象的数据做管理;而数据的来源就是经理提供的
3.管理者把这些信息进行先加以描述 比如 {姓名,年龄,业绩 …} 然后再通过一定的数据结构组织起来,比如说链表,那么对这个数据结构的操作就是对员工进行管理
那么管理的本质就是:先描述,再组织
那么操作系统充当的是什么角色呢
为什么操作系统要对软硬件资源进行管理呢?
操作系统对下通过管理好软硬件资源(手段),对上给用户提供良好的(安全,高效,稳定,功能丰富)执行环境(目的)
2.系统调用和库函数
在开发角度,操作系统对外会表现为一个整体,但是会暴露自己的部分接口,供上层开发使用,这部分 由操作系统提供的接口,叫做系统调用
系统调用在使用上,功能比较基础,对用户的要求相对也比较高,所以,有心的开发者可以对部分系统 调用进行适度封装,从而形成库,有了库,就很有利于更上层用户或者开发者进行二次开发
操作系统给我们提供非常良好的服务,并不代表操作系统会相信我们,反而 操作系统不相信任何人
比如银行这个“操作系统”,你去存钱的时候,银行不会直接让你进入他的金库然后把钱放进去,不会让你进入银行内部的电脑,自己修改账户的余额。
那银行(操作系统)既然不相信任何人,它是怎么为我们提供相应的服务呢,银行是通过柜台,为客户提供相应的服务,而操作系统就是提供特定的接口,这些接口就是系统调用(实际上就是OS设计的C函数)
但是系统调用对用户的要求相对比较高,大多数人并不知道如何正确使用这些接口,所以,有心的开发者可以对部分系统 调用进行适度封装,从而形成库,也就是用户操作的接口(比如shell外壳,lib,c库函数)。
3.进程
3.1进程概念
课本概念:程序的一个执行实例,正在执行的程序等
内核观点:担当分配系统资源(CPU时间,内存)的实体
PCB:进程信息被放在一个叫做进程控制块的数据结构中,可以理解为进程属性的集合
task_struck: Linux内核的一种数据结构,它会被装载到RAM(内存)里并且包含着进程的信息
task_ struct内容分类
标示符: 描述本进程的唯一标示符,用来区别其他进程。
状态: 任务状态,退出代码,退出信号等。
优先级: 相对于其他进程的优先级。
程序计数器: 程序中即将被执行的下一条指令的地址。
内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针 。
上下文数据: 进程执行时处理器的寄存器中的数据[休学例子,要加图CPU,寄存器]。
I/O状态信息: 包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。
记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。
其他信息
我们结和上面所说的内容, 来理解一下进程
当磁盘上的程序(文件)加载到内存上的时候, 操作系统会对这些代码和数据进行描述的工作就是把它们的信息放到一个结构体(PCB,Linux操作系统下的PCB就是task_struck)内, 然后再通过链表等数据结构对task_struck进行管理
所以说 进程=内核关于进程的相关数据结构+当前进程的代码和数据
3.2查看进程
假设我们通过某种方法开启了一个叫process的进程, 我们怎么查看这个进程的相关属性呢
1.第一种方法是通过 ps 指令来查看进程
2.通过 ls /proc
在这些属性里, 最重要的就是PID, PID是什么? 以及我们我们怎么获取PID呢?
(Process Identification)操作系统里指进程识别号,也就是进程标识符。操作系统里每打开一个[程序]都会创建一个进程ID,即PID。PID是各进程的代号,每个进程有唯一的PID编号。它是进程运行时系统随机分配的,并不代表专门的进程。在运行时PID是不会改变标识符的,但是你终止程序后再运行PID标识符就会被系统回收,就可能会被继续分配给新运行的程序。
如果要查看更详细的信息我们可以进入这个进程在这个目录下以进程PID命名的文件,比如
3.3通过系统调用来获取进程标识符
进程 id(PID) -getpid
父进程 id(PPID) -getppid
我们可以先通过man手册查看这两个函数的用法
我们以下面的一段代码进行演示
#include<stdio.h> #include<unistd.h>
#include<sys/types.h>
int main()
{
while(1)
{
printf("我的子进程是%d,父进程是%d\n",getpid(),getppid());
sleep(2);
}
return 0;
}
ctrl+c 是结束当前的进程
然后我们可以观察到两个结果:
1.再次开启相同的进程他们的PID可能是不一样的
2.他们的父进程PID是一样的
为什么他们的父进程PID是相同的呢?
我们来看一下这个父进程到底是谁,为什么每次都是同一个父进程呢
可以看出父进程就是bash(bash是命令行命令行解释器,本质上也是一个进程,命令行启动的所有的程序,最终都会变成进程,而该进程对应的父进程都是bash)
bash创建子进程的目的是为了操作不当从而导致bash挂掉
bash挂掉的后果就是导致bash无法做命令行解释,这时候就只能重新登录或者关掉Xshell 重新登陆
到这里我们就可以把 ./ (执行可执行程序)改为 将一个程序变成进程去运行
3.4批量化注释(动图演示)
1.在命令模式下输入Ctrl+v
2.然后点击通过键盘上H/J/K/L选中行
3.英文大写状态下输入i
4.接着输入//
5.最后点击Esc即可
如果这时不小心选错某一行 点击u可撤销,但是如果进行了很多步操作之后,再想去取消多行注释就要这样操作
1.在命令模式下输入Ctrl+v
2.然后点击通过键盘上H/J/K/L选中行
3.最后直接输入d
3.5创建子进程 fork()函数
3.5.1.初识fork
执行的结果是
我们不难发现通过父进程通过fork创建了一个PID为2698,PPID为2697的子进程(要注意父子进程谁先运行是不确定的)
3.5.2 fork的返回值
翻译过来就是
我们通过fork的返回值可以写一个这样的程序
按照正常的逻辑来说,应该只会执行一个执行流,但是再fork之后两个执行流都会执行,这也就得出了fork创建子进程的基本规则
1.fork之后,执行流会变成两个执行流
2.fork之后,父子进程谁先运行由调度器决定
3.fork之后,fork之后的代码是共享的,通常我们用if 和else进行分流
3.5.3fork这样做的原理
1.fork做了什么
fork为子进程创建了跟父进程指向同一位置的PCB,其中大部分属性继承自父进程,还有一部分属于子进程独有的
2.fork 如何看待 代码和数据
进程在运行的时候,是具有独立性的,父子进程运行的时候也是一样的
代码:代码是只读的(代码不会自己发生变化),共享
数据: 当有一个执行流尝试修改数据的时候,OS会自动给我们当前进程触发写时拷贝
3.fork如何理解两个返回值的问题
我们都知道,当函数内部内部准备执行return的时候,我们的主体功能已经完成
而fork本质上就是操作系统提供的一个函数,在fork创建调度完子进程后,当走到return时,return同样也是一条语句,父进程和子进程都会被调度,换句话说两个执行流都会执行return语句,从而有两个返回值。
而接受的返回值的时候,保留了两个返回值的原因就是因为触发了写时拷贝,虽然看起来两个返回值是同一个地址,但是实际上存储到不同的地址空间了
4.进程状态
4.1阻塞和挂起
阻塞:进程因为等待某种条件就绪,而导致一种不推进的状态—进程卡住了—阻塞一定是在等待某种资源—为什么阻塞? 进程要通过等待的方式,等待具体的资源被别人使用完成之后,再被别人使用,所以说阻塞就是 进程等待某种资源就绪的过程。
阻塞就是不被调度,一定是因为当前的进程需要等待某种资源就绪 — 一定是进程task_struct结构体需要在某种被OS管理的资源下排队
挂起(阻塞挂起):当内存不足时,操作系统会将暂时没在运行状态的进程的代码和数据交换到磁盘上(PCB还是在内存中)
4.2Linux进程状态
1.R状态:并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列 里
举个例子来说
2.S睡眠状态(sleeping): 意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡眠 (interruptible sleep))
本质上是一种阻塞状态
什么叫可中断呢,比如我们执行scanf的时候要等待键盘的资源,那么此时就是休眠状态,当输入完毕时,睡眠状态结束,这就叫睡眠中断
3.D磁盘休眠状态(Disk sleep)有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个状态的 进程通常会等待IO的结束。
4.T停止状态(stopped): 可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可 以通过发送 SIGCONT 信号让进程继续运行
5.t(tracing stop)追踪暂停 在gdb打打断点让程序执行到某处,这时进程的状态就是t状态
6.X (死亡状态)&& Z(僵尸状态)
Linux当进程退出的时候,一般进程不会立即彻底退出,而是要维持一个状态叫做Z,也叫做僵尸状态—这是为了方便后续父进程(OS)读取孩子进程退出的退出结果
比如我们子进程退出但是没有回收子进程,这时子进程就会进入僵尸状态
4.3僵尸状态
进程的退出状态必须被维持下去,因为子进程要告诉父进程自己的信息和运行结果,如果父进程一直不读取,那么子进程就会一直处于Z状态
维护退出状态本身就是要用数据维护,也属于进程基本信息,所以也会保存在PCB中,Z状态只要不退出,PCB一直会维护,就会造成内存资源的浪费,导致内存泄漏
5.孤儿进程
这个程序是创建一个子进程,父进程循环10次后就自动结束
值得注意的是父进程虽然终止了,但是没有处于Z状态那是因为他的父进程对他进行回收了,不同于上面子进程结束后,子进程结束后,他的父进程没有对它进行回收所以才处于Z状态。
父进程退出,子进程会被OS自动领养(通过让一号进程成为新的父进程)这个,被领养的进程就叫孤儿进程。
被领养的原因:子进程不被领养,后续退出的时候就无人回收,造成内存泄漏
杀死进程方法:kill -9 +进程PID
killall + 进程名字