深入理解计算机进程
1.初识进程
1.1什么是进程
- 课本概念:程序的一个执行实例,正在执行的程序等
- 内核观点:担当分配系统资源(CPU时间,内存)的实体。
- 磁盘里面有大量的可执行程序,要加载进内存,内存里有管理进程的驱动–PCB(process ctrl block 进程控制块),Linux下叫 task_struct
- 进程=可执行程序+该程序对应的内核数据结构。
task_ struct内容分类
- 标示符: 描述本进程的唯一标示符,用来区别其他进程。
- 状态: 任务状态,退出代码,退出信号等。
- 优先级: 相对于其他进程的优先级。
- 程序计数器: 程序中即将被执行的下一条指令的地址。
- 内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
- 上下文数据:进程执行时处理器的寄存器中的数据[休学例子,要加图CPU,寄存器]。
- I/O状态信息: 包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。
- 记账信息:可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。
- 其他信息
1.2.如何查看进程
聊到查看进程我们必须了解一个知识:进程在系统是怎么存放的?
所有运行在系统里的进程都以task_struct链表的形式存在内核里!
每个进程都有唯一的pid(process ID),就等同于学生的学号一样是唯一的。
进程显示的是当时系统内实时运行的进程信息。
查找方式
1.ps 命令:用于列出当前正在运行的进程。常用的选项包括:
-
ps aux:显示所有用户的所有进程。
-
ps -ef:显示所有进程的完整信息。
-
ps -eL:显示线程信息。
-
ps -o:自定义输出格式。
2.top 命令:动态显示系统中正在运行的进程,并提供实时的系统性能监控信息。
3.htop 命令:类似于 top 命令,但提供了更加交互式和可视化的界面。
4.pgrep 命令:根据进程的名称或其他条件查找匹配的进程。
5.pstree 命令:以树状结构显示进程之间的父子关系。
6.根据PID查找、进程名查找进程信息
-
根据ID查找 (查找pid==1234的进程信息)
ps -p 1234
pgrep 1234 -
根据文件名查找 (查找名为1234的进程信息)
pgrep 1234
ps aux | grep 1234
7.getpid()函数
1.3如何创建进程
1. ./你的程序 ./test
2.代码创建子进程
fork()函数
fork() 函数是一个在Unix-like操作系统中常见的系统调用函数,
用于创建一个新的进程,称为子进程。它通过复制当前的进程创建一个新的进程,
并且在父进程和子进程中返回不同的值,从而使得父子进程可以在不同的执行路径上同时执行。
使用 fork() 函数,父进程会创建一个与自身几乎完全相同的子进程,
包括代码、数据、文件描述符等。子进程是父进程的副本,但它有自己的进程ID(PID),
可以执行不同的代码路径。
父进程调用 fork() 函数后,会返回子进程的PID(大于0的整数),
这样父进程就能够知道自己是父进程。
子进程调用 fork() 函数后,会返回0,这样子进程就能够知道自己是子进程。
如果 fork() 函数调用失败,则返回一个负值,表示创建子进程失败。
在调用 fork() 函数后,父进程和子进程会在不同的执行路径上继续执行代码。
父子进程共享一部分资源,如打开的文件描述符、信号处理函数、文件锁等。
但是它们各自拥有独立的地址空间,即它们的数据是相互独立的。
通过 fork() 函数,我们可以实现创建多进程的程序,
父子进程可以并发执行不同的任务,从而实现并发编程和多任务处理。
需要注意的是,fork() 函数的调用可能会有一些副作用和注意事项,
比如父子进程之间的竞争条件、资源共享与同步、进程间通信等问题,开发者在使用时需要考虑这些因素。
结果:
为什么printf会执行两次?
在程序开始时,通过 fork() 函数创建了一个子进程。子进程会继承父进程的代码和数据,包括循环部分。因此,父进程和子进程都会独立执行各自的循环体,导致 printf 语句被执行多次。
父进程和子进程是并行执行的,它们的输出可以交错出现,具体顺序由操作系统调度决定。因此,在运行程序时,你可能会看到父进程和子进程的输出交替显示。举例说明:
假设有一个家庭,由父亲和儿子组成。他们每个人都有一份工作,需要每天早上去上班。
父亲进程(父进程):代表父亲去上班,他在公司工作,并且每隔一段时间报告一次他的工作进展。
儿子进程(子进程):代表儿子去上学,他在学校学习,并且每隔一段时间向父亲汇报一次他的学习情况。
这就是父进程和子进程的并行执行,它们各自独立地执行自己的循环体,因此 printf 语句会被执行多次。
1.3进程状态
进程阻塞
进程阻塞是指一个进程因等待某种事件或条件而无法继续执行的状态。
当进程执行到某个需要等待的操作时,例如等待用户输入、等待磁盘读写完成或等待某个进程的信号等,
它就会进入阻塞状态。在阻塞状态下,进程暂停执行,并将 CPU 资源让给其他可执行的进程。
进程会停止在该操作上消耗 CPU 时间,直到等待的事件或条件发生,才能继续执行。
进程阻塞的常见场景包括:等待用户输入:例如等待用户在终端输入数据。
I/O 操作阻塞:例如等待磁盘读写、网络通信等 I/O 操作完成。
同步操作阻塞:例如等待其他进程的信号或锁释放。睡眠状态阻塞:例如调用了睡眠函数 sleep()
或等待事件的函数 wait()。进程阻塞是一种合理的行为,因为在等待某些事件或条件满足之前,
进程没有必要继续占用 CPU 资源。通过阻塞状态,操作系统可以高效地利用系统资源,
同时允许其他可执行的进程继续执行。一旦等待的事件发生,
进程将从阻塞状态转换为就绪状态,并重新开始执行。
举例说明:
一个进程的运行,使用的资源可不只是在申请CPU资源,还可能申请其他资源如:
磁 盘、网卡、显示器、声卡等等,如果申请的CPU资源,暂时无法满足
(其他进程在占用),就要在运行队列里面排队,其他资源也一样。
例如在循环调用printf时,会申请显示器的资源,而CPU的运行速度远远大于显示器
的速度,所以进程就要等待,这个状态就称之为进程阻塞!
(有时候设备运行卡住了,过一会又跑起来了)
进程挂起
进程挂起是指将一个进程从内存中暂时移出,停止它的执行,
并将其状态保存到外部存储器(通常是磁盘)中,以便稍后恢复执行。
挂起的进程不会占用系统的内存和CPU资源,而是处于一种冻结状态。
挂起进程的常见场景包括:系统休眠:当计算机进入休眠状态时,
所有正在运行的进程都会被挂起,以节省能源并保护系统状态。进程调度:
操作系统可能会挂起某些进程以便给其他高优先级的进程或任务执行提供
更多的CPU时间。进程间通信:某些进程间通信机制可能会要求一个进程挂起,
直到接收到特定的消息或事件。挂起的进程在被恢复执行之前,
其上下文信息(例如寄存器状态、程序计数器等)会被保存下来,
以确保进程可以在挂起时的状态下继续执行。进程挂起的目的是在某些特定条件
下暂停进程的执行,以便合理利用系统资源或等待特定事件发生。
当条件满足或事件发生时,挂起的进程可以被恢复执行,继续其之前的操作。
Linux进程
Kernel源码分析
/*
* The task state array is a strange "bitmap" of
* reasons to sleep. Thus "running" is zero, and
* you can test for combinations of others with
* simple bit tests.
*/
static const char * const task_state_array[] = {
"R (running)", /* 0 */
"S (sleeping)", /* 1 */
"D (disk sleep)", /* 2 */
"T (stopped)", /* 4 */
"t (tracing stop)", /* 8 */
"X (dead)", /* 16 */
"Z (zombie)", /* 32 */
};
-
R运行状态(running): 并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列里。
-
S睡眠状态(sleeping): 意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡眠
-
D磁盘休眠状态(Disk sleep)有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个状态的进程通常会等待IO的结束。
-
T停止状态(stopped): 可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行。
-
X死亡状态(dead):这个状态只是一个返回状态,你不会在任务列表里看到这个状态
僵尸进程
僵尸进程(Zombie Process)是指在进程已经终止执行但其父进程
尚未对其进行善后处理(回收资源)的情况下留下的一种特殊进程状态。
当一个进程正常终止(通过调用 exit() 函数或返回 main() 函数)时,
内核会将其进程控制块(Process Control Block,PCB)标记为已终止,
但仍然保留了一些信息(如进程 ID、退出状态等)。此时,进程成为一个僵尸进程
,等待其父进程调用 wait() 或 waitpid() 函数来获取其终止状态,并回收其资源。
僵尸进程形成的主要原因是父进程没有及时回收已终止的子进程。
这可能是由于父进程没有显式地调用 wait() 或 waitpid() 函数来回收子进程,
或者是由于父进程自身已经终止而无法进行善后处理。
僵尸进程本身并不会占用系统资源,但如果大量的僵尸进程积累,
可能会耗尽系统的进程表项,导致系统无法创建新的进程。
此外,僵尸进程也可能占用一定的进程 ID,导致进程 ID 资源耗尽。
因此,僵尸进程的主要危害是对系统资源的浪费和影响系统的正常运行。
为了避免这种情况,父进程应该及时回收子进程,
可以通过调用 wait() 或 waitpid() 函数来等待子进程的终止,并释放相关资源。
X死亡状态(dead)
这个状态只是一个返回状态,你不会在任务列表里看到这个状态。
当父进程读取子进程的返回结果时,子进程立刻释放资源。死亡状态是非常短暂的,
几乎不可能通过ps命令捕捉到。
孤儿进程
所谓孤儿进程,故名思义,和现实生活中的孤儿有点类似,
当一个进程的父进程结束时,但是它自己还没有结束,
那么这个进程将会成为孤儿进程。孤儿进程会被init进程
(1号进程 可以看成是操作系统)的进程收养,当然在子进程结束时也会由init进程完成对它的状态收集工作,因此一般来说,孤儿进程并不会有什么危害.