学习 Linux 编程,非常重要的一个知识点,便是进程。那进程是什么?如何用进程编写程序呢?让我们逐步揭开进程的面纱,攻克它,使得进程为我所用。
基础内容
什么是进程
运行一个程序意味着将机器指令序列载入内存,然后让 CPU 逐条执行这些指令。进程便是程序运行时的内存空间和设置。还有一种经典定义,进程是一个执行中程序的实例。
Linux 在执行一个程序时,内核会将程序指令代码载入虚拟内存,为程序变量分配内存空间,建立记录进程信息的数据结构(如,进程 ID、用户 ID、组 ID、终止状态等)。
从内核角度来看,进程是一个个实体,内核在它们之间共享各种计算机内存资源。程序开始运行时,内核为其分配一定数量的资源。在进程的生命周期内,内核根据该进程对资源的需求,会进行相应的调整。程序终止时,内核会释放进程占有的所有资源。
进程的内存布局
进程为每个程序提供私有的地址空间。逻辑上这个地址空间分为以下几个部分:
- 代码段,存放程序的指令。
- 数据段,存放程序执行用到的数据变量。
- 堆,程序动态分配内存的区域。
- 栈,用于为局部变量和函数调用链接信息分配的内存空间。
进程用户 ID 和 组 ID
每个进程都有一组与之相关的用户 ID(UID) 和 组 ID(GID):
- 真实用户 ID 和 组 ID:用来标识进程所属的用户和组。
- 有效用户 ID 和 组 ID:进程在访问受保护资源时,会使用这两个 ID 来确定访问权限。
进程创建以及切换
进程的创建
创建一个新进程可以使用系统调用 fork()
(后面会详细介绍)。调用 fork()
的进程称为父进程,新创建的进程则称为子进程。
内核通过复制父进程来创建子进程,子进程会从父进程那里继承数据段、栈段、堆段后,可以修改这些内容,不会对父进程造成影响。
子进程可以执行与父进程共享的代码段指令,也可以调用系统函数 execve()
去加载并执行一个全新的程序。
进程 ID
每个进程都有一个唯一的整数型进程标识符(PID)。另外,每个进程还具有一个父进程标识符(PPID),用以表示请求内核创建自己的进程。
进程切换
内核为每个进程维护一个上下文。上下文是由程序正确运行所需的状态。状态包括,存放在内存中的程序代码和数据、栈、通用寄存器内容、程序计数器、环境变量、打开的文件描述符等。
当内核调度一个新进程运行时,它会抢占当前进程,使用上下文切换的机制将控制转移到新进程。进程切换步骤:
- 保存当前进程的上下文。
- 恢复先前被抢占的进程的上下文。
- 将控制传递给这个新恢复的进程。
特殊进程
init 进程
init 进程是一种特殊的进程,在系统引导时由内核会创建。也可以称这个进程为 “所有进程之父”。系统中所有的进程,不是由 init 创建,就是由其后代进程创建的。
init 进程的进程 ID 总是为 1,且是以 root 权限运行。任何进程都不能 “杀死” init 进程,只有系统关闭才能终止该进程。
init 进程的主要任务是,创建并监控系统运行所需要的一系列的进程。
守护进程
守护进程是具有特殊用途的进程,有一些独有特性:
- 不会退出。通常在系统引导时启动,在系统关闭之前,一直存在。
- 在后台运行,无控制中断供其读取或写入数据。
查看当前运行的进程
进程存在于用户空间。运行的程序的指令和数据,有一部分内容存放在用户空间。可以通过 ps (process status 进程状态的简写)指令来查看用户空间的内容。这个命令会列出当前的进程:
$ ps
PID TTY TIME CMD
7356 pts/0 00:00:00 bash
7364 pts/0 00:00:00 ps
由命令执行结果看出,有两个进程在运行:bash 和 ps 指令。每个进程都有唯一的进程标识符(PID)。这两个进程都与一个终端(TTY)相连,此处是 /dev/pts/0。接着是进程已经运行的时间。最后是创建此进程执行的命令。
ps 指令有许多选项。 支持 -a 选项,列出所有进程,包括在其他终端由其他用户运行的程序:
$ ps -a
PID TTY TIME CMD
1616 tty1 00:00:00 gnome-session-b
1622 tty1 00:00:13 gnome-shell
1637 tty1 00:00:00 Xwayland
1686 tty1 00:00:00 ibus-daemon
1689 tty1 00:00:00 ibus-dconf
1692 tty1 00:00:00 ibus-x11
1749 tty1 00:00:00 gsd-xsettings
1761 tty1 00:00:00 gsd-a11y-settin
1762 tty1 00:00:00 gsd-clipboard
1765 tty1 00:00:01 gsd-color
1766 tty1 00:00:00 gsd-datetime
1771 tty1 00:00:00 gsd-housekeepin
1772 tty1 00:00:00 gsd-keyboard
1773 tty1 00:00:00 gsd-media-keys
1774 tty1 00:00:00 gsd-mouse
1779 tty1 00:00:00 gsd-power
1782 tty1 00:00:00 gsd-print-notif
...
6925 tty2 00:00:04 gnome-software
7015 tty2 00:00:00 deja-dup-monito
7449 pts/0 00:00:00 ps
ps 指令也有 -l 选项,可以打印更多细节:
$ ps -l
F S UID PID PPID C PRI NI ADDR SZ WCHAN TTY TIME CMD
0 S 1000 7356 7346 0 80 0 - 6117 wait pts/0 00:00:00 bash
0 R 1000 7474 7356 0 80 0 - 7667 - pts/0 00:00:00 ps
S 列表示各个进程的状态,S 列中的 R 说明 ps 对应的进程正在运行。其他进程为 S,说明它们处于休眠状态。每个进程都属于由 UID 列指明的用户 ID。进程 ID (PID)和父进程 ID(PPID)也会打印显示出来。
标记为 PRI 和 NI 的列分别是进程的优先级和 nice 值。内核根据这些值来决定何时运行进程。
SZ 列表示一个进程的大小。这列的数据实际上是,这个进程占用的内存大小。
WCHAN 列显示进程睡眠的原因。上面的例子中,bash 进程处于睡眠状态,睡眠原因是等待输入。
F 和 ADDR 已经不再使用,但是为了兼容的原因而保留它们。
ps 指令还有许多其他选项,感兴趣的话可以自己查阅相关资料。
小结
本文主要介绍了进程的基础知识:
- 进程的概念
- 进程的内存布局
- 进程创建和切换
- 两种特殊进程
- 查看进程的一些信息
关注公众号【一起学嵌入式】,获取更多精彩内容