在Linux 的世界里,进程就像是一个个活跃的小生命,它们在系统的舞台上各司其职,共同演绎着复杂而精彩的计算交响乐。简单来说,进程是程序的一次执行实例。当你在 Linux 系统中运行一个程序,比如用命令行启动一个 Python 脚本,系统就会创建一个进程来执行这个脚本。程序本身只是存储在磁盘上的一组静态指令和数据,而进程则是这些指令和数据在内存中的动态运行过程。进程具有一些关键属性,这些属性就像是它们的 “身份标签” 和 “行为准则”。
比如,每个进程都有一个唯一的进程标识符(PID),这就好比每个人的身份证号码,系统通过 PID 来识别和管理不同的进程。进程还有自己的状态,常见的状态包括运行态(R)、睡眠态(S)、停止态(T)和僵尸态(Z)等 。
- 运行态表示进程正在 CPU 上执行或准备执行;
- 睡眠态则是进程在等待某个事件的完成,比如等待 I/O 操作结束;
- 停止态是进程被暂停,通常是因为接收到了特定的信号;
僵尸态比较特殊,当子进程结束运行但父进程没有回收其资源时,子进程就会进入僵尸态。进程的优先级也很重要,它决定了进程获取 CPU 时间的先后顺序。优先级高的进程会优先被调度执行,就像在医院里,急诊病人会比普通病人优先得到救治一样。在 Linux 系统中,进程的优先级可以通过 nice 值来调整,nice 值的范围是 -20 到 19,值越小优先级越高。不过,普通用户只能在一定范围内调整自己进程的 nice 值,而 root 用户则拥有更大的权限。

一、Linux进程概念
从理论角度看,是对正在运行的程序过程的抽象;
从实现角度看,是一种数据结构,目的在于清晰地刻画动态系统的内在规律,有效管理和调度进入计算机系统主存储器运行的程序。
1. 什么是进程?
- 狭义定义:进程就是一段程序的执行过程。
- 广义定义:进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。它是操作系统动态执行的基本单元,在传统的操作系统中,进程既是基本的分配单元,也是基本的执行单元。
- 进程有怎么样的特征?
- 动态性:进程的实质是程序在多道程序系统中的一次执行过程,进程是动态产生,动态消亡的。
- 并发性:任何进程都可以同其他进程一起并发执行
- 独立性:进程是一个能独立运行的基本单位,同时也是系统分配资源和调度的独立单位;
- 异步性:由于进程间的相互制约,使进程具有执行的间断性,即进程按各自独立的、不可预知的速度向前推 进
- 结构特征:进程由程序、数据和进程控制块三部分组成;
- 多个不同的进程可以包含相同的程序:一个程序在不同的数据集里就构成不同的进程,能得到不同的结果;但是执行过程中,程序不能发生改变。
2. Linux进程结构?
Linux进程结构:可由三部分组成:代码段、数据段、堆栈段。也就是程序、数据、进程控制块PCB(Process Control Block)组成。进程控制块是进程存在的惟一标识,系统通过PCB的存在而感知进程的存在。
系统通过PCB对进程进行管理和调度。PCB包括创建进程、执行程序、退出进程以及改变进程的优先级等。而进程中的PCB用一个名为task_struct的结构体来表示,定义在include/linux/sched.h中,每当创建一新进程时,便在内存中申请一个空的task_struct结构,填入所需信息,同时,指向该结构的指针也被加入到task数组中,所有进程控制块都存储在task[]数组中。
进程的三种基本状态:
- 就绪状态:进程已获得除处理器外的所需资源,等待分配处理器资源;只要分配了处理器进程就可执行。就绪进程可以按多个优先级来划分队列。例如,当一个进程由于时间片用完而进入就绪状态时,排入低优先级队列;当进程由I/O操作完成而进入就绪状态时,排入高优先级队列。
- 运行状态:进程占用处理器资源;处于此状态的进程的数目小于等于处理器的数目。在没有其他进程可以 执行时(如所有进程都在阻塞状态),通常会自动执行系统的空闲进程。
- 阻塞状态:由于进程等待某种条件(如I/O操作或进程同步),在条件满足之前无法继续执行。该事件发生 前即使把处理机分配给该进程,也无法运行。
3. 进程和程序的区别?
- 程序是指令和数据的有序集合,是一个静态的概念。而进程是程序在处理机上的一次执行过程,它是一个 动态的概念。
- 程序可以作为一种软件资料长期存在,而进程是有一定生命期的。程序是永久的,进程是暂时的。
- 进程是由进程控制块、程序段、数据段三部分组成;
- 进程具有创建其他进程的功能,而程序没有。
- 同一程序同时运行于若干个数据集合上,它将属于若干个不同的进程,也就是说同一程序可以对应多个进 程。
- 在传统的操作系统中,程序并不能独立运行,作为资源分配和独立运行的基本单元都是进程。
二、进程的查看与监控
在 Linux 系统中,了解进程的运行状态是系统管理和调试的重要环节。就像一位交通警察需要时刻了解道路上车辆的行驶情况一样,系统管理员和开发者需要通过各种工具来查看和监控进程。下面我们就来介绍几个常用的命令。
1. ps命令
ps 命令就像是进程世界的 “普查员”,它可以让我们查看系统中正在运行的进程的详细信息。通过不同的选项组合,ps 命令能够展现出进程的丰富细节。
比如,使用ps -aux命令,它会列出系统中所有用户的进程信息。这里的a表示显示所有用户的进程(包括其他用户的进程),u是以用户友好的格式显示进程信息(包括用户名、CPU 和内存使用率等详细信息),x表示显示没有控制终端的进程(通常是后台进程) 。在我的 Linux 服务器上执行这个命令,得到的部分输出如下:复制
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.0 0.1 16120 3340? Ss 08:21 0:01 /sbin/init splash
root 2 0.0 0.0 0 0? S 08:21 0:00 [kthreadd]
root 3 0.0 0.0 0 0? S 08:21 0:00 [ksoftirqd/0]
在这些输出信息中,USER表示进程所属的用户;PID是进程的唯一标识符;%CPU显示进程使用的 CPU 百分比;%MEM是进程使用的内存百分比;VSZ代表进程使用的虚拟内存大小(单位:KB);RSS是进程使用的物理内存大小(单位:KB);TTY表示进程所连接的终端设备;STAT是进程状态,常见的如S表示睡眠态,R表示运行态等;START是进程启动时间;TIME是进程占用 CPU 的累计时间;COMMAND则是启动进程的命令名称和参数。
而ps -ef命令同样用于显示所有进程的详细信息,这里的-e表示显示所有进程,包括其他用户的进程,-f是以完整格式显示进程信息 。它的输出格式与ps -aux略有不同,但包含的核心信息是一致的。例如:复制
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 08:21? 00:00:01 /sbin/init splash
root 2 0 0 08:21? 00:00:00 [kthreadd]
root 3 2 0 08:21? 00:00:00 [ksoftirqd/0]
其中,UID是进程所有者的用户 ID,PPID是父进程 ID,C表示 CPU 使用率,STIME是进程启动时间,TTY是进程关联的终端设备,TIME是进程占用的 CPU 时间,CMD是启动进程的命令名称和参数。通过这些信息,我们可以清晰地了解每个进程的基本情况。
2. top命令
top 命令则像是一个 “实时监控器”,它为我们提供了系统进程的动态实时视图,让我们可以实时了解系统中各个进程的资源占用状况,就像在观看一场实时的比赛直播一样,随时掌握进程的 “战况”。
当我们在命令行输入top命令后,会看到一个动态更新的界面,它每隔一段时间(默认 3 秒)就会自动刷新,展示最新的进程状态。界面的顶部显示了系统的一些关键信息,如系统的运行时间、当前登录用户数、系统负载(load average)等 。接下来是进程统计信息,包括总进程数、正在运行的进程数、睡眠的进程数、停止的进程数和僵尸进程数等。再下面是 CPU 使用情况的详细统计,包括用户空间占用 CPU 百分比、内核空间占用 CPU 百分比、空闲 CPU 百分比等 。还有内存和交换分区的使用情况统计。
在 top 命令的交互界面中,我们可以通过一些按键来进行各种操作。比如,按下P键,进程列表会按照 CPU 使用率从高到低进行排序,这样我们就能快速找到那些 “吃 CPU 大户” 的进程;按下M键,会按照内存使用率排序,帮助我们定位占用内存较多的进程;按下N键,会以 PID 排序 。如果我们想要监控特定用户的进程,可以按下u键,然后输入用户名,界面就会只显示该用户的进程。当我们发现某个进程占用资源过高且不需要它继续运行时,可以按下k键,然后输入该进程的 PID,再按下回车键,就可以终止该进程(默认发送的是 15 信号,即正常终止进程,如果进程没有响应,可以再次输入 9,发送强制终止信号) 。按下q键则可以退出 top 命令界面。
3. pstree命令
pstree 命令就像是一位 “家族谱绘制师”,它以树状结构展示进程之间的关系,让我们可以一目了然地看到各个进程之间的父子关系,就像查看一个家族的族谱一样清晰。
当我们执行pstree命令时,它会以init进程(在 systemd 系统中通常是systemd进程)为根节点,展示整个系统的进程树。例如,在我的系统上执行pstree命令,得到的部分输出如下:复制
systemd─┬─ModemManager───2*[{ModemManager}]
├─NetworkManager─┬─dhclient
│ ├─dnsmasq───2*[{dnsmasq}]
│ └─2*[{NetworkManager}]
├─agetty
├─atd
├─auditd───{auditd}
├─chronyd
从这个输出中,我们可以清晰地看到systemd是很多进程的父进程,比如ModemManager、NetworkManager等,而NetworkManager又有自己的子进程,如dhclient、dnsmasq等 。如果我们想要查看某个特定进程 ID 的进程树,可以使用pstree -p PID的形式,其中PID是具体的进程 ID。例如,pstree -p 1会以init进程(PID 通常为 1)为根节点,展示其完整的进程树,并在每个进程名称旁边显示其 PID 。如果我们只想查看某个用户的所有进程及其关系,可以使用pstree -u username的形式,其中username是具体的用户名。
三、进程的控制与管理
1. 进程的启动与终止
在 Linux 系统中,启动进程是一项常见的操作。最基本的方式就是在命令行直接输入命令并回车,此时进程会在前台运行 。例如,输入ls命令,系统会立即列出当前目录下的文件和文件夹,在这个过程中,命令行被该进程占用,直到ls命令执行完毕,才会返回命令提示符,在此期间我们无法在同一命令行执行其他命令。
造成进程产生的主要事件有:
- 系统初始化
- 执行进程创立程序
- 用户请求创立新进程
造成进程消亡的事件:
- 进程运行完成而退出。
- 进程因错误而自行退出
- 进程被其他进程所终止
- 进程因异常而被强行终结
如果我们希望命令在后台运行,不占用命令行,以便可以同时执行其他操作,可以在命令后面加上&符号 。比如,我们想要运行一个长时间的计算任务,如python long_computation.py &,这样该 Python 脚本就会在后台启动,命令行马上返回提示符,我们可以继续执行其他命令 。通过这种方式,多个命令可以并行执行,提高了工作效率。需要注意的是,使用&符号将命令放到后台运行后,该进程的父进程还是当前终端 shell 的进程。一旦父进程退出,它会发送 hangup 信号给所有子进程,子进程收到 hangup 信号以后也会退出。
对于已经在前台执行的命令,如果想要将其放到后台运行,可以先按下Ctrl+Z组合键暂停该进程,然后使用bg命令将其放到后台继续运行 。例如,我们在前台执行vim编辑文件时,突然需要执行其他命令,就可以按下Ctrl+Z暂停vim进程,然后输入bg命令将vim放到后台,此时可以执行其他命令。若要将后台进程恢复到前台运行,可以使用fg命令,加上对应的作业编号,作业编号可以通过jobs命令查看,jobs命令会显示所有暂停以及后台执行的命令及其编号。
当我们不再需要某个进程运行时,就需要终止它。在 Linux 中,常用kill命令和killall命令来终止进程 。kill命令用于终止指定进程 ID(PID)的进程。首先,我们需要通过ps等命令获取要终止进程的 PID,例如ps -aux | grep process_name,然后使用kill PID来终止该进程。如果进程比较顽固,无法通过正常方式终止,可以使用kill -9 PID来强制终止,-9表示发送 SIGKILL 信号,这是一种强制终止进程的方式,进程无法忽略该信号,但可能会导致数据丢失等问题,所以要谨慎使用 。比如,当一个程序出现死锁或者无响应时,我们可以使用kill -9来结束它。
killall命令则是根据进程名称来终止所有匹配该名称的进程 。它的使用相对简单,不需要先查找进程 ID,直接使用killall process_name即可。例如,要终止所有的nginx进程,只需输入killall nginx 。不过,使用killall命令时要特别小心,因为它会终止所有匹配名称的进程,如果名称不唯一,可能会误杀其他有用的进程。
2. 进程优先级调整
进程优先级决定了进程在竞争系统资源(如 CPU 时间)时的先后顺序。在 Linux 系统中,进程优先级的范围是 -20 到 19,数值越小优先级越高 。例如,一个优先级为 -10 的进程会比优先级为 5 的进程更优先获得 CPU 时间。
我们可以通过ps -l命令查看进程的优先级相关信息,其中PRI字段表示进程的实际优先级,NI字段表示进程的 nice 值 。nice 值不是进程的优先级本身,而是用于调整优先级的修正数据,进程的实际优先级(PRI)会根据 nice 值进行调整,计算公式为PRI = PRI(old) + nice,通常初始的 PRI 值为 80 。比如,一个进程的 nice 值为 0,那么它的实际优先级 PRI 就是 80;如果将其 nice 值调整为 5,那么新的实际优先级 PRI 就变为 85,优先级相对降低。
如果想要改变进程的优先级,可以使用nice命令和renice命令 。nice命令主要用于在启动进程时设置其优先级。例如,我们要以较高的优先级启动一个top命令来实时监控系统资源,可以使用nice -n -10 top,这里-n选项后面的 -10 表示设置的 nice 值,数值越小优先级越高 。这样启动的top进程就会比普通启动的top进程更优先获得 CPU 资源,能更及时地反映系统状态。
renice命令则用于修改已经在运行的进程的优先级 。假设我们有一个正在运行的进程,其 PID 为 12345,现在想要将它的优先级降低,可以使用renice -n 5 12345,这会将该进程的 nice 值设置为 5,从而调整其实际优先级 。在实际应用中,比如当我们发现某个后台进程占用过多 CPU 资源,影响到其他更重要的进程运行时,就可以使用renice命令降低它的优先级,确保系统资源能合理分配给各个进程。
3. 进程间通信
在 Linux 系统中,多个进程常常需要相互协作、交换信息,这就需要进程间通信(IPC,Inter-Process Communication)机制 。比如,一个网络服务器进程可能需要与多个客户端进程进行数据交互,一个图形界面程序可能需要与后台的数据处理进程通信来获取和展示数据。下面介绍几种常见的进程间通信方式。
(1) 管道(Pipe)
管道是一种半双工的通信方式,数据只能单向流动,而且通常只能在具有亲缘关系(如父子进程)的进程间使用 。它在内核中维护了一块内存作为缓冲区,有读端和写端 。我们可以使用pipe函数创建无名管道,父进程创建管道后通过fork子进程,子进程会继承父进程的管道文件描述符,从而实现父子进程间的通信 。例如,父进程向管道写端写入数据,子进程从管道读端读取数据。
管道的读写遵循一定规则,当管道的写端不存在时,读操作会认为已经读到数据末尾,返回的读出字节数为 0;当管道的写端存在时,如果请求读取的字节数目大于管道缓冲区的大小(PIPE_BUF),则返回管道中现有的数据字节数,如果请求的字节数目不大于 PIPE_BUF,则返回管道中现有数据字节数(当管道中数据量小于请求的数据量时)或者返回请求的字节数(当管道中数据量不小于请求的数据量时) 。向管道中写入数据时,如果管道缓冲区已满,写操作将一直阻塞,直到有数据被读取,缓冲区有空闲空间 。例如:复制
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <sys/types.h>
int main(void) {
char buf[32] = {0};
pid_t pid;
int fd[2];
// 创建无名管道
if (pipe(fd) == -1) {
perror("pipe");
return 1;
}
pid = fork();
if (pid < 0) {
perror("fork");
return 1;
} else if (pid > 0) {
// 父进程
close(fd[0]); // 关闭读端
write(fd[1], "hello", 5); // 向写端写入数据
close(fd[1]); // 关闭写端
wait(NULL); // 等待子进程结束
} else {
// 子进程
close(fd[1]); // 关闭写端
read(fd[0], buf, 32); // 从读端读取数据
printf("buf is %s\n", buf);
close(fd[0]); // 关闭读端
}
return 0;
}
在这个例子中,父进程创建管道后,通过fork创建子进程,然后父进程关闭读端,向写端写入 “hello”,子进程关闭写端,从读端读取数据并打印。
(2) 命名管道(Named Pipe,FIFO)
命名管道也是半双工的通信方式,但它允许无亲缘关系的进程间通信 。它的实质是内核维护的一块内存,表现为一个有名字的文件 。我们可以使用mkfifo函数创建命名管道,不同进程通过打开同一个命名管道文件来进行通信 。比如,一个进程以写模式打开命名管道,另一个进程以读模式打开,就可以实现数据的传输 。例如,有两个进程writer.c和reader.c,writer.c负责向命名管道写入数据,reader.c负责从命名管道读取数据:复制
// writer.c
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main() {
int fd;
char cont_w[] = "hello sundy";
// 创建命名管道
if (mkfifo("myfifo", 0666) == -1) {
perror("mkfifo");
return 1;
}
// 以写模式打开命名管道
fd = open("myfifo", O_WRONLY);
if (fd == -1) {
perror("open");
return 1;
}
write(fd, cont_w, sizeof(cont_w));
close(fd);
return 0;
}
// reader.c
/*
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main() {
int fd;
char buf[32] = {0};
// 以读模式打开命名管道
fd = open("myfifo", O_RDONLY);
if (fd == -1) {
perror("open");
return 1;
}
read(fd, buf, sizeof(buf));
printf("buf is %s\n", buf);
close(fd);
return 0;
} */
在这个例子中,writer.c创建命名管道 “myfifo” 并写入数据,reader.c打开该命名管道读取数据并打印。
(3) 共享内存(Shared Memory)
共享内存是一种高效的进程间通信方式,它允许多个进程直接访问同一块内存区域 。共享内存的实现是通过将内存段映射到共享它的进程的地址空间,这样进程间的数据传送不再需要通过内核进行多次数据复制,大大提高了通信效率 。使用共享内存时,通常需要配合信号量等机制来实现进程间的同步和互斥,防止多个进程同时访问共享内存时出现数据冲突 。例如,一个进程创建共享内存段并写入数据,其他进程可以通过映射该共享内存段来读取数据 。下面是一个简单的示例:复制
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
#define SHM_SIZE 1024
int main() {
key_t key;
int shmid;
char *shm, *s;
// 生成一个唯一的键值
key = ftok(".", 'a');
if (key == -1) {
perror("ftok");
return 1;
}
// 创建共享内存段
shmid = shmget(key, SHM_SIZE, IPC_CREAT | 0666);
if (shmid == -1) {
perror("shmget");
return 1;
}
// 将共享内存段映射到进程地址空间
shm = (char *)shmat(shmid, NULL, 0);
if (shm == (char *)-1) {
perror("shmat");
return 1;
}
s = shm;
// 向共享内存写入数据
for (int i = 0; i < 10; i++) {
sprintf(s, "This is %d\n", i);
s += strlen(s);
}
// 等待其他进程读取数据
while (*shm != '*') {
sleep(1);
}
// 取消映射
if (shmdt(shm) == -1) {
perror("shmdt");
return 1;
}
// 删除共享内存段
if (shmctl(shmid, IPC_RMID, NULL) == -1) {
perror("shmctl");
return 1;
}
return 0;
}
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
#define SHM_SIZE 1024
int main() {
key_t key;
int shmid;
char *shm, *s;
// 生成相同的键值
key = ftok(".", 'a');
if (key == -1) {
perror("ftok");
return 1;
}
// 获取共享内存段
shmid = shmget(key, SHM_SIZE, 0666);
if (shmid == -1) {
perror("shmget");
return 1;
}
// 将共享内存段映射到进程地址空间
shm = (char *)shmat(shmid, NULL, 0);
if (shm == (char *)-1) {
perror("shmat");
return 1;
}
// 从共享内存读取数据
s = shm;
while (*s != '\0') {
printf("%s", s);
s += strlen(s);
}
// 标记数据已读取
*shm = '*';
// 取消映射
if (shmdt(shm) == -1) {
perror("shmdt");
return 1;
}
return 0;
}
在这个示例中,第一个进程创建共享内存并写入数据,然后等待第二个进程读取数据并标记,第二个进程获取共享内存并读取数据,读取完成后标记数据已读取。
(4) 消息队列(Message Queue)
消息队列是由消息的链表组成,存放在内核中并由消息队列标识符标识 。它克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点 。进程可以向消息队列发送消息,也可以从消息队列接收消息 。消息队列中的消息具有类型,接收进程可以根据消息类型有选择地接收消息 。例如,在一个多进程协作的系统中,不同的进程可以根据自己的需求向消息队列发送不同类型的消息,其他进程可以根据自身需要从消息队列中获取特定类型的消息进行处理 。下面是一个简单的消息队列使用示例:复制
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>
#define MSG_SIZE 128
// 消息结构
typedef struct msgbuf {
long mtype;
char mtext[MSG_SIZE];
} msgbuf;
int main() {
key_t key;
int msgid;
msgbuf msg;
// 生成一个唯一的键值
key = ftok(".", 'b');
if (key == -1) {
perror("ftok");
return 1;
}
// 创建消息队列
msgid = msgget(key, IPC_CREAT | 0666);
if (msgid == -1) {
perror("msgget");
return 1;
}
// 填充消息
msg.mtype = 1;
strcpy(msg.mtext, "Hello, message queue!");
// 发送消息
if (msgsnd(msgid, &msg, strlen(msg.mtext) + 1, 0) == -1) {
perror("msgsnd");
return 1;
}
printf("Message sent.\n");
return 0;
}
复制
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>
#define MSG_SIZE 128
// 消息结构
typedef struct msgbuf {
long mtype;
char mtext[MSG_SIZE];
} msgbuf;
int main() {
key_t key;
int msgid;
msgbuf msg;
// 生成相同的键值
key = ftok(".", 'b');
if (key == -1) {
perror("ftok");
return 1;
}
// 获取消息队列
msgid = msgget(key, 0666);
if (msgid == -1) {
perror("msgget");
return 1;
}
// 接收消息
if (msgrcv(msgid, &msg, MSG_SIZE, 1, 0) == -1) {
perror("msgrcv");
return 1;
}
printf("Received message: %s\n", msg.mtext);
return 0;
}
在这个示例中,第一个进程创建消息队列并发送一条类型
四、特殊进程剖析
1. 守护进程
守护进程,也被称为精灵进程,是 Linux 系统中一类特殊的进程,它们如同默默守护系统的 “幕后英雄”,在后台持续运行,执行着各种重要的系统任务 。与普通进程不同,守护进程脱离于终端运行,这意味着它们不受终端的控制,也不会在终端上显示输出信息,即使终端关闭,守护进程依然能够继续运行 。就像一位忠诚的守卫,无论周围环境如何变化,始终坚守岗位。
守护进程具有一些显著的特点。首先,它们在后台长期运行,通常在系统启动时就被自动启动,并且一直持续运行到系统关闭 。比如,系统日志守护进程syslogd,它负责收集和记录系统中的各种日志信息,从系统启动的那一刻起,它就开始默默地工作,记录着系统运行过程中的点点滴滴,为系统管理员提供了重要的故障排查和系统监控依据 。其次,守护进程没有控制终端,这使得它们能够独立运行,不依赖于用户的交互操作 。
例如,网络守护进程sshd,它允许用户通过 SSH 协议远程登录到系统,即使没有用户在本地终端进行操作,sshd依然在后台监听网络端口,等待用户的连接请求,保障了远程管理的便捷性 。另外,守护进程通常以系统权限运行,这赋予了它们访问系统关键资源和执行重要任务的能力 。比如,cron守护进程,它负责执行周期性的任务,如定时备份文件、更新系统软件等,需要具备足够的权限来操作相关的文件和目录 。
在 Linux 系统中,有许多常见的守护进程。除了前面提到的syslogd、sshd和cron,还有httpd(Apache Web 服务器守护进程),它负责处理 Web 服务器的请求,使得我们能够在浏览器中访问网站;mysqld(MySQL 数据库服务器守护进程),它管理着 MySQL 数据库,为各种应用程序提供数据存储和检索服务 。这些守护进程在系统中各司其职,共同维持着系统的稳定运行 。
对于守护进程的管理,我们可以使用不同的工具和方法 。在基于 Systemd 的系统中,使用systemctl命令来管理守护进程非常方便 。比如,要启动httpd守护进程,可以使用systemctl start httpd命令;要停止它,使用systemctl stop httpd命令;要查看其状态,使用systemctl status httpd命令 。如果希望httpd守护进程在系统启动时自动启动,可以使用systemctl enable httpd命令;如果不想让它自动启动,则使用systemctl disable httpd命令 。另外,对于一些简单的守护进程,我们也可以使用nohup命令来启动,它可以让进程在后台运行,并且忽略挂断信号 。
例如,nohup my_daemon &可以将my_daemon这个守护进程在后台启动,输出信息会被重定向到nohup.out文件中 。还有supervisor,它是一个功能强大的进程管理工具,可以方便地管理和监控守护进程 。通过配置supervisor的配置文件,我们可以定义守护进程的启动命令、自动重启策略、日志输出等 。比如,在/etc/supervisor/conf.d/目录下创建一个守护进程的配置文件,然后使用supervisorctl命令来启动、停止、重启守护进程,还可以查看其状态和日志 。
2. 僵尸进程与孤儿进程
在 Linux 进程的世界里,僵尸进程和孤儿进程是两种比较特殊的进程状态,它们有着独特的产生原因和特点。
僵尸进程是指子进程已经终止运行,但父进程没有调用wait或waitpid函数来获取子进程的退出状态信息,此时子进程就会变成僵尸进程 。简单来说,就像孩子已经长大离开家(子进程结束),但家长(父进程)却没有去了解孩子的情况,孩子就一直处于一种 “悬而未决” 的状态 。
从系统的角度看,僵尸进程虽然已经不再占用 CPU 等运行资源,但它的进程描述符仍然保留在系统中,占用着进程表的一个位置 。这就好比一个已经毕业的学生,虽然不再在学校上课(不占用运行资源),但学校的学籍系统里还保留着他的学籍信息(占用进程表位置) 。如果系统中存在大量的僵尸进程,会导致进程号资源被大量占用,因为系统所能使用的进程号是有限的,当进程号耗尽时,系统将无法创建新的进程,这会对系统的正常运行造成严重影响 。
例如,我们来看下面这段 C 代码:复制
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main() {
pid_t pid = fork();
if (pid < 0) {
perror("fork");
exit(1);
} else if (pid == 0) {
// 子进程
printf("Child process, PID: %d\n", getpid());
exit(0);
} else {
// 父进程
printf("Parent process, PID: %d\n", getpid());
while (1) {
sleep(1);
}
}
return 0;
}
在这段代码中,父进程创建子进程后,子进程很快退出,但父进程没有调用wait或waitpid来处理子进程的退出,此时子进程就会变成僵尸进程 。我们可以通过ps -aux命令查看进程状态,会发现处于僵尸状态的子进程,其状态字段显示为Z 。
为了避免僵尸进程的产生,我们可以采取一些措施 。一种方法是在父进程中调用wait或waitpid函数来等待子进程的结束,并获取其退出状态信息 。wait函数会使父进程阻塞,直到有子进程结束;waitpid函数则更加灵活,可以指定等待特定的子进程,并且可以设置非阻塞模式 。例如:复制
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
int main() {
pid_t pid = fork();
if (pid < 0) {
perror("fork");
exit(1);
} else if (pid == 0) {
// 子进程
printf("Child process, PID: %d\n", getpid());
exit(0);
} else {
// 父进程
printf("Parent process, PID: %d\n", getpid());
int status;
waitpid(pid, &status, 0);
printf("Child process has exited\n");
}
return 0;
}
在这个改进的代码中,父进程调用了waitpid函数等待子进程结束,这样就不会产生僵尸进程 。
另一种避免僵尸进程的方法是利用信号机制 。当子进程结束时,会向父进程发送SIGCHLD信号,我们可以在父进程中注册SIGCHLD信号的处理函数,在处理函数中调用wait或waitpid来处理子进程的退出 。例如:复制
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <signal.h>
void sigchld_handler(int signo) {
pid_t pid;
int status;
while ((pid = waitpid(-1, &status, WNOHANG)) > 0) {
printf("Child process %d has exited\n", pid);
}
}
int main() {
struct sigaction sa;
sa.sa_handler = sigchld_handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_RESTART;
if (sigaction(SIGCHLD, &sa, NULL) == -1) {
perror("sigaction");
exit(1);
}
pid_t pid = fork();
if (pid < 0) {
perror("fork");
exit(1);
} else if (pid == 0) {
// 子进程
printf("Child process, PID: %d\n", getpid());
exit(0);
} else {
// 父进程
printf("Parent process, PID: %d\n", getpid());
while (1) {
sleep(1);
}
}
return 0;
}
在这段代码中,通过sigaction函数注册了SIGCHLD信号的处理函数sigchld_handler,当子进程结束时,会调用该处理函数来处理子进程的退出,从而避免了僵尸进程的产生 。
孤儿进程则是指父进程在子进程之前退出,导致子进程失去了父进程的管理,此时子进程就成为了孤儿进程 。这就像孩子还在成长(子进程还在运行),但家长却提前离开了(父进程先退出) 。在 Linux 系统中,孤儿进程会被init进程(在 systemd 系统中通常是systemd进程)收养,init进程会负责回收孤儿进程的资源 。例如,下面这段代码:复制
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main() {
pid_t pid = fork();
if (pid < 0) {
perror("fork");
exit(1);
} else if (pid == 0) {
// 子进程
printf("Child process, PID: %d, PPID: %d\n", getpid(), getppid());
sleep(5);
printf("Child process, PID: %d, PPID: %d\n", getpid(), getppid());
} else {
// 父进程
printf("Parent process, PID: %d\n", getpid());
exit(0);
}
return 0;
}
在这个例子中,父进程创建子进程后立即退出,子进程在睡眠 5 秒前后查看自己的父进程 ID,会发现父进程 ID 变成了init进程的 ID(通常为 1),说明子进程已经被init进程收养,成为了孤儿进程 。
孤儿进程本身对系统并没有太大的危害,因为init进程会妥善处理它们的资源回收 。但在某些情况下,我们可能需要对孤儿进程进行特殊的处理或监控 。比如,如果我们希望在孤儿进程中执行一些特定的清理操作,可以在子进程中检测父进程是否已经退出,如果发现父进程退出,就执行相应的清理代码 。例如:复制
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main() {
pid_t pid = fork();
if (pid < 0) {
perror("fork");
exit(1);
} else if (pid == 0) {
// 子进程
sleep(1);
if (getppid() == 1) {
printf("I am an orphan process. Performing clean-up...\n");
// 执行清理操作
}
while (1) {
sleep(1);
}
} else {
// 父进程
printf("Parent process, PID: %d\n", getpid());
exit(0);
}
return 0;
}
在这段代码中,子进程在睡眠 1 秒后检查自己的父进程 ID,如果发现父进程 ID 为 1,说明自己成为了孤儿进程,然后执行相应的清理操作 。
五、Linux进程实战演练
1. 案例一:优化系统性能
在日常的 Linux 系统运维中,我们常常会遇到系统性能下降的情况,这时候就需要通过各种手段来找出问题所在并进行优化。其中,找出占用资源高的进程并进行相应处理是一个重要的优化步骤。
假设我们在一台运行着多个服务的 Linux 服务器上,突然发现系统响应变得迟缓,用户反馈一些应用程序加载缓慢。这时候,我们首先想到的就是使用top命令来查看系统中各个进程的资源占用情况。在终端中输入top命令后,我们会看到一个动态更新的界面,其中%CPU和%MEM列分别显示了每个进程占用 CPU 和内存的百分比。通过观察这两列数据,我们发现一个名为big_computation.py的 Python 进程占用了高达 80% 的 CPU 资源,同时占用了大量的内存 。复制
top - 10:15:23 up 2 days, 1:23, 2 users, load average: 2.50, 2.00, 1.50
Tasks: 200 total, 2 running, 198 sleeping, 0 stopped, 0 zombie
%Cpu(s): 80.0 us, 10.0 sy, 0.0 ni, 5.0 id, 5.0 wa, 0.0 hi, 0.0 si, 0.0 st
MiB Mem : 7914.2 total, 123.4 free, 6543.2 used, 1247.6 buff/cache
MiB Swap: 2048.0 total, 2048.0 free, 0.0 used. 1024.0 avail Mem
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
12345 user1 20 0 512340 204800 12340 R 80.0 2.5 5:10.00 python big_computation.py
这个进程可能是导致系统性能下降的 “罪魁祸首”。如果这个进程是一个临时的计算任务,且目前并非急需它运行,我们可以考虑终止它以释放系统资源。通过ps -ef命令进一步确认该进程的详细信息,获取其 PID 为 12345,然后使用kill命令来终止它 。复制
ps -ef | grep big_computation.py
user1 12345 1 80 10:00 ? 00:05:10 python big_computation.py
kill 12345
执行kill命令后,再次查看top命令的输出,发现系统的 CPU 使用率和内存占用率都有了明显的下降,系统响应速度也恢复正常 。
如果这个进程是一个长期运行的服务,我们不能直接终止它,但可以尝试调整其优先级,让它少占用一些 CPU 资源,以保证其他更重要的进程能够正常运行 。比如,使用renice命令将其优先级降低。假设我们希望将其 nice 值调整为 10(nice 值越大优先级越低) 。复制
renice -n 10 12345
执行上述命令后,再次观察top命令的输出,会发现该进程的优先级降低,CPU 使用率也有所下降,系统的整体性能得到了优化 。通过这样的实战操作,我们能够更好地掌握 Linux 系统中进程的监控和优化技巧,确保系统的稳定高效运行 。
2. 案例二:进程间通信实现
进程间通信在 Linux 系统中有着广泛的应用场景,下面我们通过一个简单的示例来展示使用管道实现进程间数据传递的方法。
假设我们有一个数据生成进程和一个数据处理进程,数据生成进程不断生成一些随机数,数据处理进程需要接收这些随机数并进行平方计算 。我们可以使用管道来实现这两个进程之间的数据传递。
首先,我们创建一个 C 语言程序来实现数据生成进程(producer.c) :复制
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <time.h>
int main() {
int pipe_fd[2];
if (pipe(pipe_fd) == -1) {
perror("pipe");
return 1;
}
pid_t pid = fork();
if (pid < 0) {
perror("fork");
return 1;
} else if (pid == 0) {
// 子进程,作为数据处理进程
close(pipe_fd[1]); // 关闭写端
int num;
while (read(pipe_fd[0], &num, sizeof(int)) > 0) {
int result = num * num;
printf("Processed result: %d\n", result);
}
close(pipe_fd[0]); // 关闭读端
} else {
// 父进程,作为数据生成进程
close(pipe_fd[0]); // 关闭读端
srand(time(NULL));
for (int i = 0; i < 5; i++) {
int num = rand() % 100;
write(pipe_fd[1], &num, sizeof(int));
printf("Generated number: %d\n", num);
sleep(1);
}
close(pipe_fd[1]); // 关闭写端
wait(NULL); // 等待子进程结束
}
return 0;
}
在这个程序中,首先创建一个管道,然后通过fork函数创建子进程。父进程作为数据生成进程,关闭管道的读端,生成 5 个随机数并通过管道的写端发送给子进程 。子进程作为数据处理进程,关闭管道的写端,从管道的读端读取数据,对读取到的数据进行平方计算并打印结果 。
编译并运行这个程序:复制
gcc -o producer producer.c
./producer
运行结果如下:复制
Generated number: 34
Processed result: 1156
Generated number: 78
Processed result: 6084
Generated number: 12
Processed result: 144
Generated number: 91
Processed result: 8281
Generated number: 56
Processed result: 3136
从运行结果可以清晰地看到,数据生成进程不断生成随机数并发送给数据处理进程,数据处理进程成功接收并进行了相应的处理 。通过这个简单的案例,我们展示了使用管道实现进程间通信的基本方法,在实际应用中,可以根据具体需求对这个示例进行扩展和优化,以满足更复杂的进程间通信场景 。
0 条评论