目录
前言:
我们此时已经掌握了很多关于需要了解操作系统的前置知识,接下来我们需要来深入了解操作系统了。
一、冯诺依曼体系结构:

输入设备:键盘、鼠标、网卡、磁盘、摄像头、话筒。
输出设备:显示器、磁盘、网卡、打印机。
存储器就是内存,CPU(运算器+控制器),外存就是像磁盘的一些东西,CPU可以直接向外设发送控制信息。
当CPU要对输入设备的数据进行处理时,数据会先从输入设备到存储器,之后CPU进行处理;之后处理好的数据会到存储器,最终到达输出设备。
CPU在数据层面,不会和外设直接打交道,只会和内存进行交互。任何程序,运行时都必须先被加载到内存,这是由体系结构决定的(冯诺依曼体系结构)。
把输入数据输入到内存中,叫做input;把内存数据输出到输出设备中,叫做output。

此时我们再次对IO进行理解(这里是站在内存的角度来看的)。
因为外设速度慢,但是CPU速度快,所以引入存储器的概念。内存是CPU和外设之间的一个巨大的缓存。我们来看一个内存金字塔:

当数据在计算机内部流转的时候,本质就是在不同的设备间进行拷贝。
二、操作系统:
操作系统简称OS(operate system)。
操作系统包括:内核(进程管理,内存管理,驱动管理) + 其他程序(例如函数库,shell程序等)。
1.操作系统是什么?

2.为什么要有操作系统?
大部分硬件接口都集成在主板上,每一种硬件都要有自己的驱动程序。此时操作系统就可以通过这些驱动程序来操作这些硬件。也可以理解为操作系统的触手。

操作系统对下进行软硬件管理,对上进行对客户提供良好的运行环境。
操作系统对下进行高效的,稳定的、安全的软硬件管理(手段),对上进行对客户提供良好的,稳定的,安全的运行环境(目的)。
3.操作系统是如何管理下层的?
这里举一个例子:学校中,有校长,辅导员和学生。校长是管理者,以决策为主;辅导员一般执行工作,所以以执行为主。辅导员对上采集获取数据,对下执行决策工作。我们可以把辅导与理解为驱动程序,校长理解为操作系统,硬件理解为学生。
我们从上往下理解:此时有2w个学生,校长该如何管理呢?校长以前是一个程序员,于是就想:这些学生有很多共同属性,于是就把这些学生定义为一个结构体(学过其他语言的都知道也就是类),因为Linux底层是C语言实现,但是如果多个学生(结构体对象)该如何进行管理呢?用数组的话是固定大小的,外设超出了上限就无法连接外设了。于是我们在该结构提的内部添加了一个属性,就是该结构体的指针,之后可以通过类似链表的方式来组织学生。
当有一个新学生入校时,就把他加入到链表尾部(尾插)。聪明的学生已经想到了,外设也是这样,因为这些外设有共同的属性,方法,也可以通过将其描述一个结构体对象。当有一个新外设的时候,就把该外设的结构体对象链接到链表尾部。
所以你可以发现一个规律:都是将这些设备进行先描述,再组织的。所以得出一个结论:任何计算机对象,管理思路都遵守该原则!
这里就有一个解释了为什么所有主流语言都提供:面向过程、标准库。
也为什么明白了为什么要有数据结构了。最开始只用编程语言和操作系统的学科,最后人们发现所有最后都会有面向对象,和数据结构。于是就有了数据结构学科和面向对象的语言。
4.操作系统是如何对上层提供服务的?
接下来我们再用一个银行的模型来举例,此时你想向银行进行存取操作,行长不相信任何人,于是就提供了窗口服务(系统调用接口)。

所以可以类比操作系统:

所以操作系统就和银行一样不相信任何人,会把自己封装起来。我们不能直接对硬件进行写入,我们必须要经过操作系统。
操作系统必须向上提供各种接口,方便上层使用。只开放system call(系统调用)。因为这个接口是C语言的接口。
比如C语言的fopen就是分装系统调用接口open函数的。
所以所有软件底层,都必须和C直接或者间接相关!
可是也有人压根不会这些系统调用。比如一个老大爷,字都不会写,使用银行窗口的成本也很高。此时就需要行长对银行新增一个大堂经理的角色。这个大堂经理就是来辅助这些老大爷的。也就是说在系统调用层又进行了封装。

所以我们在座的各位都是老大爷, 不排除有大佬。
这里再插一嘴。图形化界面中,比如我们对一个页面进行关闭,其实就是对显示器进行清屏并且底层调用系统调用接口并对此程序进行关闭。在手机上开发的图形化界面,内核使用的是Linux,这个就是安卓。
三、进程:
1.什么叫做进程?
进程(Process) 是操作系统中的一个核心概念,指的是 正在运行的程序的实例。每个进程都有独立的内存空间、资源和执行状态,操作系统通过进程来管理和调度程序的执行。
内核观点:担当分配系统资源(CPU时间,内存)的实体。
注意:进程和程序不是一个概念!一个是正在执行的程序(进程);一个是没有执行的程序(程序)。
2.进程管理
操作系统中有一个模块:进程管理。
操作系统是如何对进程进行管理的呢?先描述,在组织!
一定是在系统中定义一个结构体,之后以某种方式进行管理(比如链表):

此时我们写了一个code.c文件,经过编译生成myexe可执行程序,并放在磁盘上。此时我们执行这个可执行程序(./myexe)。
因为操作系统也是一个软件,对于进程当然要进行管理。当执行一个进程时,操作系统会先把这个进程描述为一个结构体对象。

图中有很多关于进程结构体中的属性,这里我们只看看注释即可。后程我们会对这些重要的进程属性进行讲解。
此时如果我们打开了多个进程,就会通过其中的结构体指针组织这些进程。此时会先通过操作系统中的struct task_struct* tasks_list找到第一个进程对象。

当然有些教材中会把描述进程的结构体叫做PCB(process control block)。
**为什么要有PCB(struct task_struct)?因为OS要管理进程(先描述,在组织)!**至于属性,我们之后再谈。
注意这个进程结构体(为方便描述,我们后面统称结构体为类)和磁盘没有任何关系,因为操作系统也是软件,这就是在该软件上定义的类。
3.重新定义进程
进程 = 内核数据结构(task_struct) + 程序的代码和数据
四、进程调度(宏观认识)
这里我们先来了解概念,操作系统中,调度是指一种资源分配和管理的机制。
进程调度主要是决定就绪状态的进程(已经在内存中,准备好运行的进程)获得 CPU 资源的顺序。
所以当CPU调度进程的时候,并不是直接把文件的代码和数据直接执行的,而是通过拿到对应的类对象进行调度的。
进程会根据task_struct属性被OS调度器调度,运行。
五、PCB属性:
task_struct是Linux内核的一种数据结构,它会被装载到RAM(内存)里并且包含着进程的信息。
task_struct内容分类:
- 标示符: 描述本进程的唯一标示符,用来区别其他进程。
- 状态: 任务状态,退出代码,退出信号等。
- 优先级: 相对于其他进程的优先级。
- 程序计数器: 程序中即将被执行的下一条指令的地址。
- 内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
- 上下文数据: 进程执行时处理器的寄存器中的数据。
- I/O状态信息: 包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。
- 记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。
- 其他信息
1.PID
Process ID,进程 ID,我们先观察下面的例子。
此时我们写一个myproc.c文件(写成死循环),并让其跑起来:


此时该程序被运行,也就是一个进程,我们要去查看其相关属性,打开另一个Xsehll窗口。
1.1ps命令(补充):
ps命令能查看当前系统中进程状态的命令。
-a:显示与终端有关的所有进程,包括其他用户进程。
-j:以作业格式显示进程信息。会显示进程信息的会话ID(SID)、进程组ID(PGID)、控制终端(TTY)等信息。
-x:显示没有控制终端的进程。这些进程通常是系统后台服务或者守护进程。
我们使用ps指令加上管道符来查看当前执行的进程。
bash
ps ajx | grep myproc

此时直接使用ps ajx是查看当前系统的所有进程。

此时我们最好保留头部首行,Linux中命令可以以";"(或者"&&")来执行多个相关命令:

tips: grep是过滤命令
此时我们将myproc进程关闭,再次查询,你会发现每次都会存在一个grep进程,这是为什么?

此时我们只想查看对应进程不要grep,此时就可以通过-v(反选)来过滤掉grep进程。

所以我们平时使用的命令(比如:ls、ll等)命令,都是一个进程。
Linux中进程一般分为两种:
- 瞬时进程(执行完就退出,ls,pwd等) 。
- 常驻进程(用户退出才退出)。
进程标识符其实就是PID,此时我们继续让myproc运行。

1.2进程获取PID
那么一个进程当然有资格知道自己的PID,就好比一个学生要知道自己的学号一样。
所以系统提供了一个系统调用函数getpid来获取本进程PID。
man 查看标准库函数或系统调用的时候发现出现错误,比如在查看free()函数时的错误为:No manual entry for free,即没有free()这个函数的联机手册。
原因就在于系统中没有安装完全联机手册,解决方案:
在命令行输入以下命令:sudo yum install man-pages即可。
不知道什么原因,小编电脑上的Xshell无法显示其需要引入的头文件包括其函数使用方法,只能在centOS上查看,字体变小希望大家体谅。

此时我们修改myproc.c的代码并打印出pid,所以要引入头文件:

在另外一个窗口中查看进程:

1.3终止进程
此时我们想终止该进程?除了在执行命令的窗口输入Ctrl+C,还可以在另外一个窗口中发送信号来终止进程,使用kill -l来查看有哪些信号:

我们主要来看9(SIGKELL 后续会讲解信号)。我们通过kill -9 pid来杀死指定信号。

就和Windows中使用资源管理器关闭一个进程一样。
1.4proc目录
因为Linux中一切皆文件。在根目录下有一个很特殊的目录proc:

proc目录全称为process目录,就是进程目录。Linux中为了方便我们去查进程的属性,就把进程的信息以文件的方式呈现出来。


所以当我们启动一个进程,就会在proc目录下创建一个以其PID为名的目录,方便我们去查看该进程的属性。

此时我们再次执行myproc文件,之后进入proc/pid(该进程的PID)查看这个进程的具体属性。

其中exe目录记录的是可执行程序的具体所在位置,此时我们在另外一个对话框中把myexe可执行文件清理了,并观察结果:

这是因为进程此时已经被加载到了内存中,我们删除的是磁盘上的文件,所以程序依旧运行。但是重新启动则会报错。
ps/proc都可以看进程的参数和属性,其实系统本身就是提供proc去查看进程的属性的,但是我们一般使用ps,因为ps底层就是用proc实现的。
proc不是磁盘的文件,而是内存数据。
这里举一个例子:一个村有20个低保名额。领导调查以后发现20人中,只有10人是这个村的,另外的10个是其他村的。这里就可以理解为你感觉所有文件都在磁盘上,其实并不是。
2.cwd
current word dir:当前工作目录。
如果我们直接创建文件,就会默认在当前目录下新建一个文件。此时我们修改代码,在代码中新建一个log.txt文件。


**更准确的说是进程的cwd。**我们也可以主动改变当前启动进程的工作路径,我们使用系统调用提供的方法,这里有一个系统调用接口chdir。
注意:因为可执行程序的位置是固定的,与cwd无关,所以可以进行修改!


此时我们再次make:

此时我们终止程序并查看根目录中有没有log.txt文件。

此时我们可以更换cwd:


此时终止进程并前往家目录中查看文件是否被创建:

3.ppid
ppid:父进程ID
在Linux系统中,启动之后,新创建任何进程的时候,都是由父进程创建的。
我们使用getppid系统调用接口获取该进程的父进程。


shell是所有外壳命令行解释器的统称,Linux系统中用到的一般就是bash!
bash为了帮我们进行命令行解释,又为了命令行解释不影响到bash自身,于是就帮我们创建了子进程。


由子进程执行我们的代码。
bash进程又是如何创建子进程的呢?接下来我们会使用系统调用创建进程。
-bash前面的-是代表使用命令行终端进行查看的:
3.1fork系统调用
fork用于创建子进程。

此时我们查看fork函数的返回值:

注意,重要的事情说2两遍:创建成功给子进程返回0,给父进程返回子进程的pid;失败给父进程返回-1。
这里会颠覆你的认知,不过不要慌,我们以后会解决的,先听。
此时我们尝试在.c文件中创建子进程:

cpp
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
int main()
{
printf("I am a process, pid:%d, ppid:%d\n", getpid(), getppid());
pid_t id = fork();
(void)id;
printf("I am a 分支!pid:%d, ppid:%d\n", getpid(), getppid());
sleep(1);
return 0;
}

这里大家要仔细阅读上面这句话,并且看清楚父子pid。
一个父进程可以有多个子进程,所以Linux中所有进程也是树形结构。
此时为了更方便的观察,我们修改代码:

cpp
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
int main()
{
printf("I am a process, pid:%d, ppid:%d\n", getpid(), getppid());
pid_t id = fork();
if (id > 0)
{
//此为创建成功的子进程PID
//因为给父进程返回子进程的代码
//所以这里执行父进程的代码
while(1)
{
printf("我是父进程, pid:%d, ppid:%d, ret id:%d\n",getpid(), getppid(), id);
sleep(1);
}
}
else if(id == 0)
{
while(1)
{
//这里给子进程返回0
//所以执行的是子进程的代码
printf("我是子进程, pid:%d, ppid:%d, ret id:%d\n",getpid(), getppid(), id);
sleep(1);
}
}
return 0;
}
注意代码中有两个死循环!

惊呆了,竟然写了一个死循环竟然还能跑另一个死循环, 而且同时进行。
我们先简要分析一下:

此时我们再次查看进程:

这里你就会发现,难道它有两个返回值?但是C语言我们从来没有听说过有两个返回值。这个等我们以后再去解释。
父进程有多个孩子,就必须拿到子进程的PID对其管理。fork之后会创建两个进程,这两个进程具有父子关系,一般而言,代码是共享的,而数据时各有一份的。

所以fork如果不细分,就会在父子进程中同时执行fork之后的代码,这里我们在代码中添加一个全局变量,让父进程只读,子进程修改:

cpp
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
//定义一个全局变量
int gval = 0;
int main()
{
printf("I am a process, pid:%d, ppid:%d\n", getpid(), getppid());
pid_t id = fork();
if (id > 0)
{
//此为创建成功的子进程PID
//因为给父进程返回子进程的代码
//所以这里执行父进程的代码
while(1)
{
printf("我是父进程, pid:%d, ppid:%d, ret id:%d, gval:%d\n",getpid(), getppid(), id, gval);
//父进程只读
sleep(1);
}
}
else if(id == 0)
{
while(1)
{
//这里给子进程返回0
//所以执行的是子进程的代码
printf("我是子进程, pid:%d, ppid:%d, ret id:%d, gval:%d\n",getpid(), getppid(), id, gval);
//子进程修改
gval++;
sleep(1);
}
}
printf("\n");
return 0;
}

这也就验证了代码是共享的,数据是个自私有一份的。
这是为什么?首先进程具有独立性,多个进程运行时是互不影响的,即使是父子进程。
具体更详细的解释,我们之后再深入讲解。
3.2创建多进程
此时我们使用C++创建多个进程创建多个进程。此时我们在新建的mulprocess目录中新建一个myprocess.cc。文件(也就是C++文件),并把之前的makefile文件拷贝到当前目录中。


此时对C++代码不能使用gcc编译,而要是用g++编译,所以还要再次修改makefile内容:

大家一定要记得先安装g++:
bash
sudo yum install -y gcc-c++
我们需要使用c++11的标准库。

我们需要使用c++11的标准库,此时我们在myprocess.cc中写入以下内容:
cpp
#include<iostream>
#include<unistd.h>
#include<sys/types.h>
#include<vector>
//展开命名空间
using namespace std;
//创建10个进程
const int num = 10;
void SubProcessRun()
{
while(true)
{
cout << "I am sub process, pid: " << getpid() << ",ppid: " << getppid() << endl;
sleep(1);
}
}
int main()
{
vector<pid_t> allchild;
for (int i = 0; i < num; ++i)
{
pid_t id = fork();
if (id == 0)
{
//子进程
SubProcessRun();
}
//这里父进程执行
allchild.push_back(id);
}
//父进程
cout << "我所有的孩子是:";
for (auto child: allchild)
{
cout << child << " ";
}
cout << endl;
while(true)
{
cout << "我是父进程,pid: " << getpid() << endl;
sleep(1);
}
return 0;
}


此时直接Ctrl+x直接杀死全部进程, 注意此时并没有阻塞状态,子进程并不影响父进程执行,同时进行。
3.3folk函数返回值(宏观认识)
接下来我们就要研究一个函数为什么会有两个返回值?

至于怎么做到的?我们后期会讲到虚拟地址(虚拟内存)会把这个坑填上, fork之后,父子进程有调度器自主决定谁先运行。
六、命令总结:
ps : 查看当前系统中进程状态的命令。
-a:显示与终端有关的所有进程,包括其他用户进程。
-j:以作业格式显示进程信息。会显示进程信息的会话ID(SID)、进程组ID(PGID)、控制终端(TTY)等信息。
-x:显示没有控制终端的进程。这些进程通常是系统后台服务或者守护进程。
kill : 给指定进程(PID)发送信号
-l:查看所有信号
总结:
大致讲述了操作系统和进程的一些属性,当然,很多东西并没有讲清楚,我会在后续的博客讲这些坑一一填补。也希望各位认真阅读,相信会使你受益匪浅。大型连续Linux篇,持续为您播出!