chapter 3 Unix/Linux进程管理
学习笔记:Unix/Linux进程管理
摘要
- 本章深入探讨Unix/Linux中的进程管理。
- 它涵盖了多任务处理的原理和引入进程概念。
- 使用编程示例演示了这些概念。
- 解释了多任务处理、上下文切换以及各种与进程相关的技术。
3.1 多任务处理
- 多任务处理涉及同时执行多个独立的活动。
- 在计算中,它指的是同时执行多个独立任务。
- 单处理器系统一次只能执行一个任务,多任务处理是通过上下文切换实现的。
- 上下文切换 改变了执行环境,从一个任务切换到另一个任务。
- 快速上下文切换创建了同时执行的错觉(并发)。
- 多处理器系统允许任务在不同CPU上并行运行。
3.2 进程概念
- 操作系统是多任务处理系统,任务称为进程。
- “任务”和“进程”通常可以互换使用。
- 进程被定义为图像的执行,表示操作系统内核将其视为使用系统资源的单个实体(例如,内存、I/O设备、CPU时间)。
- 每个进程由唯一的数据结构表示,称为进程控制块(PCB)或任务控制块(TCB),也称为PROC结构。
- PROC结构包含有关进程的关键信息,如进程ID(pid)、父进程ID(ppid)、状态、优先级和用于执行的堆栈。
- 在实际操作系统中,PROC结构可能包含许多其他字段。
- 为演示目的,本示例引入了一个简单的PROC结构。
示例PROC结构:
typedef struct proc {
struct proc *next; // 下一个PROC指针
int *ksp; // 保存的堆栈指针
int pid; // 进程ID
int ppid; // 父进程ID
int status; // PROC状态(FREE|READY等)
int priority; // 调度优先级
int kstack[1024]; // 进程执行堆栈
} PROC;
- PROC结构的字段包括“next”(指向下一个PROC的指针)、“ksp”(保存的堆栈指针)、“pid”(进程ID)、“ppid”(父进程ID)、“status”(当前状态)、“priority”(调度优先级)和“kstack”(执行堆栈)。
- 在系统中通常有有限数量的PROC结构,由
PROC proc[NPROC];
表示。 - 在单CPU系统中,一次只能执行一个进程,操作系统内核使用全局的PROC指针(例如“running”)指向当前正在执行的进程。
- 在具有多个CPU的多处理器操作系统中,进程可以在不同CPU上并行运行,每个CPU都有自己的“running”指针。
注意: 本章为理解Unix/Linux中的进程管理打下了基础,提供了有关多任务处理和进程概念的洞察。它还介绍了用于表示的简化PROC结构。
3.7 Unix/Linux中的进程
3.7.1 进程的起源
- 当操作系统启动时,OS内核的启动代码通过强制方式创建了一个PID=0的初始进程。
- 这是通过分配一个PROC结构(通常是proc[0]),初始化PROC内容,并将running指向proc[0]来完成的。
- 因此,系统开始执行初始进程P0。
- P0继续初始化系统,包括系统硬件和内核数据结构。
- 然后,它挂载根文件系统以使文件对系统可用。
- 初始化系统后,P0分叉了一个子进程P1,并切换了进程以在用户模式下运行P1。
3.7.2 INIT和守护进程
- 当进程P1开始运行时,它将其执行图像更改为INIT程序。
- 因此,P1通常被称为INIT进程,因为其执行图像是init程序。
- P1开始分叉许多子进程。P1的大多数子进程旨在提供系统服务。
- 它们在后台运行,不与任何用户进行交互,因此称为守护进程。
- 守护进程的示例包括syslogd(日志守护进程)、inetd(Internet服务守护进程)、httpd(HTTP服务器守护进程)等。
3.7.3 登录进程
- 除了守护进程,P1还分叉了许多LOGIN进程,每个终端一个,供用户登录。
- 每个LOGIN进程打开与其自己终端相关联的三个文件流。这三个文件流是stdin(标准输入)、stdout(标准输出)和stderr(标准错误消息)。
- 每个文件流是指向进程HEAP区域中的FILE结构的指针。每个FILE结构记录一个文件描述符(编号),stdin为0,stdout为1,stderr为2。
- 然后,每个LOGIN进程在stdout上显示“login:”,等待用户登录。
- 用户帐户维护在文件/etc/passwd和/etc/shadow中。
- 每个用户帐户在/etc/passwd文件中都有一行,其中包含用户名、x(表示登录时检查密码)、gid(用户组ID)、uid(用户ID)、home(用户主目录)和program(用户登录后要执行的初始程序)。
- 其他用户帐户信息在/etc/shadow文件中维护。
- 当用户尝试使用登录名和密码登录时,Linux将同时检查/etc/passwd和/etc/shadow文件以验证用户。
3.7.4 Sh进程
- 当用户成功登录时,LOGIN进程获取用户的gid和uid,从而成为用户的进程。
- 它更改到用户的主目录并执行列出的程序,通常是命令解释器sh。
- 用户进程现在执行sh,因此通常称为sh进程。
- 它提示用户输入要执行的命令。
- sh本身直接执行一些特殊命令,如cd(更改目录)、exit(退出)、logout等。
- 大多数其他命令是各种bin目录中的可执行文件,例如/bin、/sbin、/usr/bin、/usr/local/bin等。
- 对于每个(可执行文件)命令,sh分叉一个子进程并等待子进程终止。
- 子进程将其执行图像更改为命令文件并执行命令程序。
- 当子进程终止时,它唤醒父sh,收集子进程终止状态,释放子PROC结构,并提示进行另一个命令等。
- 除了简单命令,sh还支持I/O重定向和多个由管道连接的命令。
3.7.5 进程执行模式
- 在Unix/Linux中,进程可以在两种不同的模式下执行:内核模式(Kernel mode)和用户模式(User mode),简称为Kmode和Umode。
- 在每种模式下,进程都有一个执行图像。
- Umode中的进程图像通常是不同的,而在Kmode中它们共享相同的Kcode、Kdata和Kheap(这是OS内核的一部分),但每个进程都有自己的Kstack。
- 一个进程在其生命周期中多次在Kmode和Umode之间迁移。
- 每个进程首先以Kmode进入世界并在Kmode中执行所有有趣的事情,包括终止。
- 在Kmode中,它可以非常容易地从Umode切换,通过更改CPU的状态寄存器从K到U模式。
- 但一旦在Umode中,由于明显的原因,它不能随意更改CPU的状态。
- Umode进程可能只通过以下三种可能的方式之一进入Kmode:
- 中断(Interrupts):中断是来自外部设备的信号,请求CPU服务。在Umode执行时,CPU的中断被启用,以便对任何中断作出响应。发生中断时,CPU将进入Kmode以处理中断,导致进程进入Kmode。
- 陷阱(Traps):陷阱是错误条件,如无效地址、非法指令、除以0等,被CPU识别为异常,导致CPU进入Kmode来处理错误。在Unix/Linux中,内核陷阱处理程序将陷阱原因转换为信号编号并将信号传递给进程。对于大多数信号,进程的默认操作是终止。
- 系统调用(System Calls):系统调用是一种机制,允许Umode进程进入Kmode以执行
内核功能。当进程完成执行内核功能时,它返回Umode并带有所需的结果和返回值,通常为成功为0或出错为-1。出错时,外部全局变量errno(在errno.h中)包含一个识别错误的错误代码。用户可以使用库函数perror("error message");
来打印错误消息,后跟描述错误的字符串。
- 每当进程进入Kmode时,它可能不会立即返回Umode。在某些情况下,它根本不会返回Umode。例如,_exit()系统调用和大多数陷阱会导致进程在内核中终止,因此永远不会再返回Umode。当进程即将退出Kmode时,OS内核可能会将进程切换到运行较高优先级的其他进程。
注意: 了解进程的起源、初始化、守护进程、登录进程、sh进程和执行模式是深入了解Unix/Linux中的进程管理的重要组成部分。
3.8 Linux进程管理系统调用
在Linux中,进程管理是一个关键的操作,它涉及创建、执行、等待、终止等各种操作。以下是有关进程管理的相关系统调用的概要:
3.8.1 fork()
fork()
是一个库函数,它创建一个子进程并返回子进程的PID,如果fork()
失败则返回-1。- 用户在
/etc/security/limits.conf
文件中可以设置每个用户同时运行的最大进程数。用户可以通过ulimit -a
命令查看各种资源限制。 - 子进程是通过在父进程中调用
fork()
创建的,子进程在创建时复制了父进程的用户态图像,包括代码、数据等。 fork()
使子进程继承了父进程的所有打开文件,因此父子进程可以在同一个终端上进行输入和输出。
3.8.2 exec()
exec()
系列函数用于改变进程的用户态图像,将其替换为一个新的可执行文件。它们包括execl
,execlp
,execle
,execv
,execvp
。execve()
是这些函数的底层系统调用,它使用参数filename
指定要执行的可执行文件,argv
是一个包含命令行参数的数组,env
是一个包含环境变量的数组。exec()
函数执行后,原进程的用户态图像被替换,但进程的PID保持不变。
3.8.3 进程执行顺序
- 在调用
fork()
后,子进程和父进程都会竞争CPU时间。进程的执行顺序取决于它们的调度优先级,这些优先级会动态变化。
3.8.4 进程终止
- 进程可以正常终止或异常终止。
- 正常终止通常发生在
main()
函数成功返回后,父进程调用exit()
或_exit()
。 - 异常终止通常是由于错误或异常引起,如非法地址、特权违规等。进程可能会接收到信号,导致它异常终止。
3.8.5 wait()
wait()
系统调用用于等待子进程的终止,返回子进程的PID和退出状态。- 当子进程终止时,它变成一个僵尸进程,父进程可以使用
wait()
将其清理。
3.8.6 子进程管理
- 自Linux内核版本3.4起,Linux处理孤儿进程的方式有所不同。一个进程可以自我定义为一个子管理进程,这个进程不再是孤儿进程,而是由最近的一个自定义子管理进程托管。如果没有其他子管理进程,孤儿进程仍然由INIT进程托管。
- 子管理进程的概念有助于用户空间服务管理器能够追踪它们启动的服务,并避免孤儿进程的问题。
3.8.7 环境变量
- 环境变量在登录配置文件和
.bashrc
脚本中设置,用于定义进程的执行环境,如PATH
和HOME
。 - 环境变量可以通过
env
命令查看。环境变量是以KEYWORD=string
的形式定义的。 - 环境变量可以在进程之间传递,并且在C程序中通过
env[]
参数访问。
3.9 I/O 重定向
3.9.1 文件流与文件描述符
-
文件流:
- 在 sh shell 中,进程有三个文件流,用于终端 I/O:stdin(标准输入)、stdout(标准输出)和stderr(标准错误)。
- 这些流表示为指向执行映像堆区中 FILE 结构的指针。
- 每个流都有关联的属性,如 fbuf、counter、index 等。
- 这些流对应的文件描述符(fd)分别为 stdin 为 0,stdout 为 1,stderr 为 2。
-
文件描述符:
- 每个文件流对应于 Linux 内核中的已打开文件。
- stdin、stdout 和 stderr 的文件描述符分别为 0、1 和 2。
- 当进程 fork 子进程时,子进程继承父进程的已打开文件和文件描述符。
3.9.2 文件流 I/O 与系统调用
- 当进程使用函数(如
scanf("%s", &item)
)时,它尝试从 stdin 输入数据。 - 如果 FILE 结构的 fbuf 为空,它会发出一个读取系统调用,以从文件描述符 0(键盘或终端)读取数据。
3.9.3 重定向 stdin
- 要将输入从文件重定向到原始来源以外,可以替换文件描述符 0。
- 使用
close(0)
关闭文件描述符 0 并打开文件(例如int fd = open("filename", O_RDONLY)
) 来替换它。 - 或者,可以使用
int fd = open("filename", O_RDONLY)
,close(0)
和dup(fd)
来复制文件描述符。
3.9.4 重定向 stdout
- 要将标准输出重定向到文件,可以替换文件描述符 1。
- 使用
close(1)
关闭文件描述符 1 并打开文件(例如open("filename", O_WRONLY|O_CREAT, 0644)
) 来替换它。 - 类似地,还可以将 stderr 重定向到文件。
3.10 管道
3.10.1 管道基础
- 管道是单向进程间通信通道。
- 它有读端和写端。
- 写入写端的数据可以从读端读取。
- 管道用于进程之间交换数据。
3.10.2 Unix/Linux 中的管道编程
- 管道由系统调用支持,例如
pipe(pd)
用于创建管道,并返回两个文件描述符,pd[0] 用于读取,pd[1] 用于写入。 - 进程只能在管道上是读取者或写入者,不能同时两者兼顾。
- 创建管道后,进程 fork 子进程以共享管道,并且每个进程必须关闭不需要的描述符。
- 写入者进程使用
write(pd[1], wbuf, nbytes)
写入管道,读取者使用read(pd[0], rbuf, nbytes)
读取。 - 管道同步读取者和写入者进程。
3.10.3 管道命令处理
- 管道用于命令行,例如
cmd1 | cmd2
。 - Shell 运行 cmd1 和 cmd2 在不同进程中,它们通过管道连接,cmd1 的输出成为 cmd2 的输入。
3.10.4 命名管道
- 命名管道,也称为 FIFO,具有名称,并作为文件系统中的特殊文件存在。
- 用于非关联进程之间的通信。
- 命名管道使用
mknod
或系统调用创建。 - 进程可以像操作常规文件一样从命名管道读取和写入数据,同步由内核处理。
苏格拉底挑战
点击查看问答详情
Unix/Linux进程管理知识点
当然,我可以用一种苏格拉底式的方法来提问您关于Unix/Linux进程管理的知识点。让我们开始吧:
问题1:什么是进程(Process)在Unix/Linux操作系统中的定义?
非常好,您已经正确描述了进程的定义。接下来的问题:
问题2:每个进程在Unix/Linux系统中都有一个唯一的标识符,这个标识符是什么,以及它的作用是什么?
PID的作用是标识系统中的每个进程,使操作系统能够管理和监控进程。PID可以用来识别进程的所有权和资源使用情况,以及与其交互的其他进程。
PID是一个非负整数,并且在一个特定的时间内,相同的PID不能分配给两个不同的进程。当一个进程终止时,其PID也会被释放,可供其他进程使用。
非常好,您已经正确解释了PID的作用和性质。接下来的问题:
问题3:在Unix/Linux系统中,进程可以通过哪些方式创建?请列举一些常见的方法。
fork()系统调用:用于在父进程中创建一个新的子进程,新进程继承了父进程的资源并独立运行。
exec()系列系统调用:用于执行一个程序文件,并将其替换为当前进程,即在当前进程中加载另一个程序运行。
system()库函数:用于执行一个系统命令,并返回执行结果。
popen()和pclose()函数:用于创建一个进程并连接到其标准输入、标准输出或标准错误流。
pthread_create()函数:用于创建一个新的线程,这个线程可以在当前进程中独立运行。
非常棒,您详细列举了Unix/Linux系统中创建进程的常见方法。接下来的问题:
问题4:在Unix/Linux系统中,进程之间如何进行通信以便交换数据和协调工作?
命名管道(named pipe):命名管道也是一种进程间的通信方式,可以实现双向数据传输。不同于管道只能在有亲缘关系的进程之间使用,命名管道可以在不同进程之间传输数据。
消息队列(message queue):消息队列是一种支持异步通信的进程间通信方式,进程可以通过向消息队列中发送消息将数据传递给其他进程。
共享内存(shared memory):共享内存是一种进程间通信方式,在内存中创建一块特殊的区域(共享内存区),多个进程都可以访问该区域的数据。
信号(signal):信号是一种进程间通信方式,它用于通知某个进程发生了某种事件。比如,一个进程可以向另一个进程发送一个信号,让该进程停止或中断运行。
网络套接字(socket):套接字是一种在网络上进行进程间通信的方式,可以通过网络传输数据。
RPC(Remote Procedure Call):RPC是一种实现远程过程调用的技术,多个进程可以通过远程调用的方式来互相通信和协调工作。
非常详细的解答,您已经涵盖了Unix/Linux系统中不同的进程间通信方式。接下来的问题:
问题5:在Unix/Linux中,进程的状态可以分为哪些主要类别,以及这些状态分别表示什么意思?
阻塞态(Blocked):进程由于某些原因无法运行,等待某个事件(如IO完成)的发生,或者等待某个资源(如共享内存)的释放,当事件或资源可用时,进程会被唤醒并重新进入运行态。
暂停态(Stopped):进程暂停运行,可能是被用户或其他进程发送信号使得其停止运行,等待被继续执行。
跟踪态(Traced):进程处于被其他进程跟踪的状态,通常用于调试程序。
僵尸态(Zombie):进程已经完成了自己的任务,但其父进程还未对其进行回收,因此进程已经成为了僵尸状态。在Linux中,僵尸进程会一直占用系统资源,如果过多的僵尸进程,则会导致系统性能下降甚至奔溃。
待处理状态(Waiting):主要指处于等待CPU运行的状态,一般是指等待进程调度的状态。
非常好,您对进程状态的解释非常清楚。接下来的问题:
问题6:在Unix/Linux中,如何查看当前正在运行的进程列表以及它们的状态信息?
-e:列出所有进程
-f:显示进程的详细信息,包括所属用户、进程ID、状态等
-l:以长格式显示进程详细信息,包括命令行参数、CPU时间等
-aux:列出所有进程的详细信息,包括用户、进程ID、CPU占用率等
top命令:top命令可以动态地显示当前系统的进程信息,包括CPU占用率、内存占用率、进程的状态等。主要命令选项包括:
-d:设置刷新间隔时间,默认是3秒
-p:根据PID来监控指定的进程
htop命令:htop命令与top类似,但它提供了更加直观、易用的交互式界面。它可以显示当前进程的资源占用情况、进程的树形结构等信息。
pstree命令:pstree命令可以显示进程树形结构,以及每个进程的状态信息。它可以帮助用户更好地理解进程之间的关系。
非常详细的回答,您已经提供了查看当前运行进程和它们状态信息的几种常用方法。接下来的问题:
问题7:在Unix/Linux中,如何终止或结束一个进程?
- SIGTERM(15):默认信号,用于请求进程正常终止,并允许进程做完清理工作。
- SIGKILL(9):强制终止信号,会立即杀死进程,不允许进程做清理工作,可能会有数据丢失或损坏。
-
pkill命令:pkill命令可以根据进程名或其他条件来杀死对应的进程。它比kill命令更加方便,可以避免手动查找进程ID的麻烦。
-
killall命令:killall命令可以根据进程名来杀死所有同名的进程。类似于pkill命令,可以方便地批量杀死进程。
您提供了多种方式来终止或结束一个进程,包括使用kill
命令、pkill
命令和killall
命令。接下来的问题:
问题8:进程的优先级是什么,以及如何调整进程的优先级?
很好,您已经解释了进程的优先级以及如何调整它们。接下来的问题:
问题9:什么是守护进程(Daemon Process)?它们在Unix/Linux系统中有什么特点和用途?
守护进程的特点和用途如下:
-
后台运行:守护进程在后台运行,不会与用户进行交互,通常没有控制终端。
-
无需用户登录:守护进程不依赖于用户的登录状态,可以在系统启动时自动启动,或者由其他进程触发启动。
-
长期运行:守护进程通常会持续运行,直到系统关闭或被手动停止。
-
无输出:守护进程通常不会向控制台输出信息,而是将运行日志等信息记录到日志文件中。
-
独立性:守护进程通常是独立于特定用户的,它们不受用户登录或注销的影响。
-
系统服务:守护进程经常用于提供系统服务,如网络服务、打印服务、定时任务等。
-
管理:守护进程的启动、停止和管理通常由操作系统或相关管理工具负责。
非常详细和准确的解释,您已经很好地描述了守护进程的特点和用途。接下来的问题:
问题10:什么是孤儿进程(Orphan Process)和僵尸进程(Zombie Process),它们在Unix/Linux系统中的产生原因是什么?
-
孤儿进程:指父进程还未运行完毕就已经结束了,导致子进程没有了父进程依靠而成为孤儿进程。孤儿进程将被init进程接手,成为init进程的子进程。
-
僵尸进程:指子进程执行完毕,但是父进程还没有通过wait等函数等待它的结束状态,导致子进程成为僵尸进程。僵尸进程占用系统的进程表项,因此是非常不利于系统性能和资源的,应该及时清理。
造成孤儿进程和僵尸进程出现的原因是父子进程之间的通信问题。当子进程终止或者父进程结束时,内核会向父进程发送一个信号,以通知父进程子进程已经结束,父进程需要执行wait等函数获取终止状态并从系统进程表中删除子进程的信息。如果父进程没有处理这个信号或者没有合适地使用wait等函数,则会导致子进程变成僵尸进程,如果父进程已经结束,则会导致子进程成为孤儿进程。
为了避免产生孤儿进程和僵尸进程,建议在父进程中调用wait等函数,以等待子进程的终止状态,并及时从进程表中清除子进程的信息,从而释放系统资源。同时,如果需要长时间运行的进程,则可以考虑使用守护进程等技术,以确保进程的稳定性和正常运行。
非常清晰和全面的回答,您已经很好地解释了孤儿进程和僵尸进程的概念以及它们的产生原因。如果您还有任何关于Unix/Linux进程管理的问题,或者需要更多信息,请继续提问。
对于Unix/Linux进程管理的知识点,您的理解非常出色。您详细解释了进程的定义、状态、通信方式、优先级、终止方法、守护进程、孤儿进程以及僵尸进程等关键概念,这些回答都非常准确和清晰。您展示了对Unix/Linux进程管理的深刻理解,这将有助于您更好地应用和管理进程在Unix/Linux系统中的运行。继续加强这些知识,并将它们应用到实际场景中将会使您在Unix/Linux系统管理领域更加熟练。如果您还有其他问题或需要更多的信息,请随时提问。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· .NET周刊【3月第1期 2025-03-02】
· [AI/GPT/综述] AI Agent的设计模式综述