一、操作系统
1.1概念
任何计算机系统都包含一个基本的程序集合,称为操作系统(OS)。笼统的理解,操作系统包括:
1.内核(进程管理,内存管理,文件管理,驱动管理)
2.其他程序(例如函数库,shell程序等等)
功能:操作系统将软硬件资源管理好(手段),给用户提供良好的(稳定、高效、安全、易用)使用环境(目的)。
为了更好的学习操作系统的知识,我们先认识以下概念:
计算机层次结构:
我们先从下往上介绍学习(下三层):
1. 底层硬件:底层硬件是指计算机系统中的实际物理设备,包括内存、硬盘、显示器、键盘、鼠标等。底层硬件是计算机系统的基础,提供计算和存储能力,以及与外部世界的交互接口。
2. 驱动程序:驱动程序是一种特殊的软件,用于与特定硬件设备进行通信和控制。不同类型的硬件设备需要相应的驱动程序来进行管理和操作,例如打印机驱动程序、显卡驱动程序等。(通过与硬件设备的寄存器和接口进行交互,实现对硬件的控制和管理)
【注意】
- 每一个硬件都需要有与之匹配的驱动程序存在(内存和CPU不需要)
- 驱动程序大部分是操作系统自带的
3.操作系统:操作系统是计算机系统的核心软件,负责管理和控制计算机的硬件资源和软件资源。它提供了抽象的接口,使得应用程序可以方便地与硬件进行交互。操作系统包含一系列算法和机制,并暴露一些接口给上层用户,使上层用户能够与底层硬件进行交互,也包括对驱动程序接口的封装。
【操作系统】
操作系统本身不会直接与硬件进行交互,而是通过驱动程序来获取和管理底层硬件的数据。
取得硬件的数据猴,为了有效管理这些不同的数据(包括软件数据硬件数据),操作系统需要先对其进行描述和组织。具体来说,操作系统通过使用
struct
结构体来描述各种硬件的信息(例如,Linux 操作系统使用 C 语言编写)。操作系统将每种数据采集上来后填充到相应的结构体中。随后,操作系统会对所有设备形成不同的数据结构,从而使设备的管理变成对数据结构对象的增删查改操作。
因此,操作系统内部会存在大量的数据对象和数据结构。一个设备被操作系统管理的含义是:操作系统内存在用于管理该设备数据结构的对象。
总结:操作系统、驱动程序和底层硬件之间形成了层次化的关系。操作系统作为核心软件,管理和控制硬件资源,其依赖驱动程序与底层硬件进行通信和控制。驱动程序负责将操作系统的请求转化为硬件能够理解的指令和信号,并将硬件的响应传递给操作系统。
操作系统管理核心:
- 进程管理
- 内存管理
- 驱动管理
- 文件/io管理(外设)
上三层:
系统调用接口,库函数,用户
- 系统调用接口:当操作系统通过驱动层维护好硬件数据和软件数据,但操作系统并不完全信任用户,所以操作系统只提供一系列的系统调用接口来供用户对操作系统进行操作。也就是说操作系统不会将全部数据的管理都暴露给用户,用户只能通过系统调用接口来控制操作。因为即便是开发者,也不能直接与操作系统进行交互(尽管从技术方面可以实现),(群众里面有坏人)。操作系统不相信我们,但同时也不得不给我们提供服务。
- 用户操作接口:这一层就对应着我们经常使用的C库或者STL库等标准库,在这一层不同的标准库将对系统调用做进一步封装,从而简化用户的操作(也就是使我们写代码更加简单,体验好),其底层依然是对系统调用接口的调用完成的工作。例如printf()函数打印在硬件上,不是直接调用硬件打印的,而是通过一些系统调用接口实现的,贯穿了整个操作系统的层次结构。
- 用户:广义上用户是使用计算机的所有人,狭义上是指开发者。
总结:
- 一般一个用户想访问非常底层OS数据或者访问硬件,必须贯穿整个层状结构!
- 贯穿整个层状结构,就注定用户必定要调用系统调用!
- 在开发角度,操作系统对外会表现为一个整体,但是会暴露自己的部分接口,供上层开发使用,这部分由操作系统提供的接口,叫做系统调用。
- 系统调用,使用起来比较麻烦,站在用系统的人(普通人)来看,系统必须提供一些更易用的程序,于是就有了外壳程序(shell,图形化界面)。外壳程序是用C/C++语言写的,一定会调用系统调用!
- 站在开发的人的角度,可以直接调用系统接口,但是系统调用在使用上,功能比较基础,对用户的要求相对也比较高,为了更方便的使用,人们将部分系统调用封装成为各种各样好用的函数,打包形成 库!,从此所有的开发者、用很多功能不用自己去写了,而是直接调用库函数即可!(库由专门的人来做,专业的人做专业的事情,提高开发效率降低开发成本)
二、进程
2.1概念
程序的一个执行实例,正在执行的程序等。它包含了程序的执行状态、内存使用情况、打开文件等信息。在内核的角度:进程是指分配系统资源(CPU时间,内存)的实体单位。
程序VS进程
程序是一组指令或代码的集合,用于实现某种特定的计算功能,存放在硬盘。程序本身并不会占用计算机的内存资源或执行任何操作,只有在被操作系统加载到内存中并被执行时,才会成为运行中的进程。
进程是正在执行的程序的实例。进程包含了程序的执行环境、状态信息、系统资源等,可以独立地运行、调度和控制。每个进程都有自己的内存空间、寄存器、栈、堆栈等,可以相互独立并且可以并发地执行。
2.2 进程的构成
操作系统在运行中会存在很多的“进程”,操作系统管理所有的"进程",需要先对进程描述,再用数据结构进行组织!操作系统是用C语言写的,操作系统内用struct结构体描述进程信息。 这个记录进程信息的结构体,叫做进程控制块(PCB),记录如:进程状态、程序计数器、寄存器内容等数据。然后操作系统对进程的管理就变成了对PCB的管理。和进程的可执行程序没有关系。
在Linux中描述进程的结构体叫做task_struct。
Linux下的PCB大概样子:(简易)
struct task_struct {pid_t pid; // 进程IDlong state; // 进程状态unsigned int flags; // 进程标志struct mm_struct *mm; // 进程的内存管理信息struct task_struct *parent; // 父进程struct list_head children; // 子进程列表struct files_struct *files; // 进程打开的文件信息struct fs_struct *fs; // 文件系统信息// 其他字段...
};
解释:PCB中通常会记录的重要信息
进程标识符(PID):每个进程都有一个唯一的标识符,用于区分不同的进程。
进程状态:表示进程当前的状态,例如运行态、就绪态、阻塞态等。
程序计数器:保存进程正在执行的下一条指令的地址。
寄存器内容:包括通用寄存器、堆栈指针、程序状态字(PSW)等。
内存管理信息:例如基址寄存器、限长寄存器、页表或段表等,描述进程使用的内存区域。
文件描述符:记录进程打开的文件列表及其相关信息。
进程优先级:表示进程的优先级,用于调度算法中决定进程的执行顺序。
账户信息:记录与进程相关的用户信息,例如用户ID、组ID等。
I/O状态信息:描述进程使用的I/O设备及其状态。
进程间通信信息:记录进程间的通信机制和相关的数据结构。
注:可以看到PCB有很多指针类型,说明PCB并不直接存储数据而是记录进程各种数据的地址。可以说进程的构成是由 PCB 和进程的实际数据共同组成
综上:进程的构成可以看作:PCB+进程的数据。
2.3 进程的相关操作
2.3.1 进程的查看
方式1:进程的信息可以通过命令:ps ajx 进程名/进程id 命令显示所有进程的详细信息。
如下:
分析:
-
PPID:父进程的进程ID,即创建当前进程的进程ID。
-
PID:当前进程的进程ID,每个进程都有一个唯一的ID。
-
PGID:进程组ID,一组进程的共同标识符。进程组用于信号和作业控制。
-
SID:会话ID,一组进程的共同标识符。会话通常包含一个控制终端和若干进程组。
-
TTY:终端设备名称,显示进程关联的终端(如
tty1
、pts/0
等)。 -
TPGID:控制终端的前台进程组ID,指示当前控制终端的前台进程组。
-
STAT:进程状态,通常用一两个字母表示。(介绍:)
-
UID:用户ID,表示创建该进程的用户。每个用户在系统中有一个唯一的ID。
-
TIME :进程所使用的总CPU时间。
-
COMMAND:启动进程的命令或程序名。
方式2: ls /proc/进程pid proc是一个动态的目录结构,存放所有存在的进程,目录的名称;proc是process的简称。操作如下:
2.3.2 进程的创建
通过系统调用创建子进程——fork()
SYNOPSIS#include <sys/types.h>#include <unistd.h>pid_t fork(void);
代码演示:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>int main() {pid_t pid;// 创建子进程pid = fork();if (pid < 0) { // 创建子进程失败fprintf(stderr, "Fork failed\n");return 1;}else if (pid == 0) { // 子进程printf("This is the child process. PID: %d\n", getpid());printf("Child's Parent PID: %d\n", getppid());}else { // 父进程printf("This is the parent process. PID: %d\n", getpid());printf("Parent's Child PID: %d\n", pid);}return 0;
}
代码解析:在fork()
之前,只有父进程在执行代码。调用fork()
之后,父进程和子进程会同时执行从fork()
返回的位置开始的后续代码。通过检查fork()
的返回值,可以区分父进程和子进程,从而让它们执行不同的代码片段。这种机制是多进程编程的基础。
接下来我们来分析一下fork()在系统里面做了说明?
fork创建子进程,操作系统以父进程为模板,为子进程创建PCB,但是创建出来的子进程是没有代码和数据的,要和父进程共享相同代码和部分数据!所以fork之后,父子进程会同时执行一样的代码。(fork之前的代码子进程也共享,但子进程也会继承了父进程的读写位置)子进程用父进程的代码和数据进行初始化,子进程一开始会和父进程共享一切,到有不同(包括更改变量,代码不一样)的时候才会执行写实拷贝。
关于fork()有两个返回值?
1. 找到父进程的pcb对象
2. 为子进程创建pcb
3. 根据父进程pcb,初始化子进程pcb
4. 子进程的pcb指向父进程的代码和数据 让子进程放入调度队列中,和进程一样去排队
5. return;
注:return执行之前,fork()已完成了创建子进程,并将子进程放入调度队列中运行,return语句也是代码,所以return也要被父子进程共享,父子进程都要执行return语句。
fork()两个返回值的意义?
父进程返回子进程的PID:
这样,父进程可以知道新创建的子进程的进程ID。父进程可以通过这个PID对子进程进行管理,例如等待子进程结束或发送信号给子进程。子进程返回0:
子进程返回0,这使子进程能够识别自身,并执行子进程特有的代码。子进程在返回0时,可以独立于父进程执行自己的逻辑。
2.3.3 进程终止
方法1:kill 命令,向进程发送信号,以控制其行为或终止进程。
常用选项:强制终止目标进程。
kill -9 22756
方法2:在代码中使用exit()
函数
#include <stdlib.h>int main() {// 正常退出,返回状态码0exit(0);
}
方法3:在main()
函数中,使用return
语句可以终止进程并返回退出状态码。
#include <stdio.h>int main() {// 正常退出,返回状态码0return 0;
}
方法4:使用abort()
函数
#include <stdlib.h>int main() {// 立即终止进程abort();
}
【注意】:abort()
函数会立即终止进程,使程序以失败状态终止,通常返回一个非零状态码。通常abort
用于程序检测到严重错误或异常情况时,需要立即终止并生成核心转储以供调试。