本文演示所用系统为 CentOS 7.6

1. 操作系统

操作系统是不会直接对用户提供服务的。因为这样会暴露自己的底层实现,对系统稳定性造成了威胁

image-20220924091531854

操作系统是通过系统调用层的方式对外提供接口服务的。

这就好比你是通过前端按钮来使用一个网站的功能,而通常你是看不到网页的后端实现的。

Linux 系统的底层是用 C 语言写的,所以这些接口服务本质上就是一些 C 语言的函数。这些函数用于操作系统的各种管理。

我们学习 Linux 的系统编程,本质上是在学习这些和系统对接的函数。

1.1 编程语言和系统对接

不同的操作系统,其提供的各种管理硬件的函数是不同的。这时候我们的 C/C++ 等其他语言想和系统对接(如 printf 打印到屏幕上)就需要在底层帮用户管理好这些系统接口的调用。当我们使用这些语言的时候,就不需要自己手动去调用

1.2 描述进程 - PCB

PCB 并不是那些绿油油的电路板,这里指的是 process control block

进程是有一个担当分配系统资源(CPU 时间,内存)的实体

  • 进程信息被放在一个叫做进程控制模块的数据结构中,可以理解为进程属性的集合
  • 在 Linux 中的 PCB 其实就是一个结构体 task strcut,包含了这个进程的各种信息。

task_struct 的内容大家可以上网搜搜,能力强的朋友可直接去看 Linux 的源码。

1.3 组织进程

我们可以在 Linux 的源码中找到组织进程的方式。所有运行在系统里面的进程都是以 task_struct 为成员的链表形式存在内核中。下面是关于这个结构体的一部分解析

  • 标示符:描述本进程的唯一标示符,用来区别其他进程
  • 状态:任务状态,退出代码,退出信号等。
  • 优先级:相对于其他进程的优先级。
  • 程序计数器:程序中即将被执行的下一条指令的地址。
  • 内存指针:包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
  • 上下文数据:进程执行时处理器的寄存器中的数据 [休学例子,要加图 CPU,寄存器]。
  • I/O 状态信息:包括显示的 I/O 请求,分配给进程的 I/O 设备和被进程使用的文件列表。
  • 记账信息:可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。
  • 其他信息

相关解析可以看看这篇博客【传送门

1.4 查看进程

1.4.1 ps 命令

可以用 ps 命令来查找进程信息。下面这个指令是显示所有的进程

bash
1
ps axj

系统会打印下面的很多进程信息,这就好比 windows 下的任务管理器

image-20220811090300115

如果我们使用一个 while(1) 的死循环函数,运行的时候就变成了一个进程了。我们可以用下面的命令来查找特定的进程信息

bash
1
ps axj | grep test

image-20220811090538553

上图中我们搜索 test,出现了两个进程。第一个进程很明显是我们运行的可执行程序。那么第二个是什么呢?

实际上,所有的指令都是一个进程。只不过 ls 这种类型的命令很快就能执行完毕。

这样一来,我们便可以确认,出现的第二个进程实际上是我们执行这条搜索语句出现的。可以用下面的这个指令来屏蔽 grep 的结果,只显示我们自己的那个程序。

bash
1
ps axj | grep test | grep -v grep

最后一个指令的意思是忽略掉包含 grep 的结果,现在就不会显示第二个 grep 的进程了

image-20220811091231767

1.4.2 proc 目录

还可以通过 /proc 系统文件夹来查看系统进程信息。这里面的内容都是一个实时的进程信息

image-20220811090141498

每一个进程都有一个自己的 PID(process id),用来标识唯一的进程

可以用下面的这个指令来显示一个提示信息,其中的 && 代表逻辑与,只有第一个命令执行成功,才会执行第二个命令

bash
1
ps ajx | head -1 && ps ajx | grep test | grep -v grep

image-20220811092151510

这里就告诉我们了这个命令的 PID 是什么,即第 2 位数字


如果你需要查看 PID 为 1 的进程,则打开对应进程的文件夹

image-20220811090118889

上面我们搜到的进程 PID5408,查看对应文件夹可以看到下面的内容

image-20220811092354284

当我们关闭了./test 进程在去查找,会发现没有这个路径了

image-20220811092513042

在每一个进程文件夹中都有一个 cwdexe

  • cwd 其指向的是进程当前的工作路径
  • exe 指向的是进程对应可执行程序的磁盘文件

比如现在我使用 ps 命令查找到了下面这个 python 代码进程

image-20220924085753986

使用 ls -l /proc/19423 命令打开对应文件夹,便可以看到 cwdexe 的指向

image-20220924085852444

这代表该进程是用 python3.10 进行执行的,其工作目录为 cwd 指向的路径

pid、工作路径等等信息都存放在进程的 task_struct

1.4.3 C 语言代码获取 ppid 和 pid

除了在命令行中输入命令以外,我们还可以通过 C 语言代码中和系统通信的库函数来获取当前进程的 ppid和pid

c
1
2
3
4
5
6
7
8
9
10
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>

int main()
{
printf("pid: %d\n", getpid());
printf("ppid: %d\n", getppid());
return 0;
}

image-20220830100222676

使用一个 while(1) 循环,我们就可以看看这个进程是否和我们用 ps 搜出来的结果相同

image-20220830100501201

image-20220830100507478

可以看到,搜寻出来的结果和该程序自己打印的结果是一样的

实际上,我们所有在命令行上执行的程序,都是 bash(即当前命令行)的子进程


1.5 fork 通过系统调用创建父子进程

如果你用过 gitee 或者 github,想必对 fork 并不陌生。在 git 托管网站上,我们 fork 别人的仓库,便会在自己的账户中出现一个别人仓库的 “子仓库”。我们可以在这个 “子仓库” 里面修改一部分信息,再创建一个 pull request 合并入被 fork 的仓库

而在 Linux 系统中,fork 的作用便是可以创建一个父子进程,这两个进程相互独立,且有很多特殊的地方等着我们的探索

比如:fork 具有两个不同的返回值

下面是一个示例代码

c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>

int main()
{
pid_t id = fork();
//id=0 子进程;>0为父进程
if(id == 0)
{
//child
while(1)
{
printf("子进程,pid: %d, 父进程是: %d\n", getpid(), getppid());
sleep(1);
}
}
else{
//parent
while(1)
{
printf("父进程,pid: %d, 父进程是: %d\n", getpid(), getppid());
sleep(1);
}
}
}

运行上面的代码,你会发现父子进程竟然交叉运行了!而且这两个进程都有不同的 pid。其中子进程的 ppid 即为父进程的 pid

image-20220830101839334

奇怪,我们的 while 循环明明是写在 if 里面的啊?为什么 else 里面的 while 也被正常执行了呢?

1.5.1 man fork

这便需要我们了解一下 fork 到底是何方神圣了。

bash
1
man fork

如果你出现 No manual entry for fork 的报错,在 CentOS 下请尝试执行下面的命令

bash
1
sudo yum install -y man-pages

image-20220830102447021

同时可以看到关于 fork 的返回值的描述

image-20220830102840823

这便能告诉我们,为啥 fork 下方同一个 id 值使用打印会返回不同的结果;以及 if else 被交叉运行的原因。

  • fork 之后,父子进程共享代码,都会执行后面的 if else 语句
  • fork 之后,父子进程的返回值不相同,所以 if else 语句进入的模块也不相同

fork 进程给父进程返回子进程pid,方便父进程管理自己的子进程。这是因为父进程必须要有标识子进程的方法!

  • 一个父亲可以有多个孩子,需要 pid 来进行管理
  • 一个子进程只能有 1 个父亲,所以用 0 来标识子进程创建成功即可。它可以用 ppid 方便的找到自己的父进程。

1.5.2 fork 做了什么

fork 会调用系统的 OS system call,创建一个子进程

  • task_struct + 父进程代码和数据
  • task_struct + 子进程代码和数据

子进程的代码和数据大多数都是从父进程继承下来的,不过 pid 和 ppid 肯定不会继承。其内部的变量/数据和父进程独立(这个后续的博客会涉及)

  • 当 fork 创建好子进程并进行 return 的时候,它的功能就已经完成了
  • 此时还会将子进程放入运行队列

了解了这个之后,再来理解一下进程是如何被运行的👇

2. 如何理解进程被运行

在我们的系统中,每一个 CPU 都有一个运行队列

这个运行队列之中存放的便是 task_struct,系统会依次运行每一个进程。

image-20220830112455633

在上面我们的 fork 创建子进程之后,便会把子进程放入运行队列

image-20220830120248799

所以实际上,fork 并不是有两个返回值,而是在先运行了父进程后,又创建了一个子进程。这两个进程共享代码,而且它们都是挂载在同一个 bash 命令行上,才会出现上述交替打印的情况。


结语

下篇博客是有关进程状态的内容,阿巴阿巴,好久没写博客了

QQ图片20220418131327