目录
基本概念
描述进程-PCB
task_struct-PCB的一种
task_struct内容分类
查看进程
通过系统目录查看
通过ps命令查看
通过系统调用获取进程的PID和PPID
通过系统调用创建进程- fork初始
Linux进程状态
运行状态(Running)- R
浅度睡眠状态(Light Sleep)- S
深度睡眠状态(Deep Sleep)- D:
暂停状态(Paused)- T
僵尸状态(Zombie)- Z
死亡状态-X
僵尸进程
僵尸进程的危害
孤儿进程
基本概念
课本概念:一个运行起来(加载到内存)的程序或者在内存中的程序
内核观点:担当分配系统资源(CPU时间,内存)的实体。
只要写过代码的都知道,当你的代码进行编译链接后便会生成一个可执行程序,这个可执行程序本质上是一个文件,是放在磁盘上的。当我们双击这个可执行程序将其运行起来时,本质上是将这个程序加载到内存当中了,因为只有加载到内存后,CPU才能对其进行逐行的语句执行,而一旦将这个程序加载到内存后,我们就不应该将这个程序再叫做程序了,严格意义上将应该将其称之为进程。
描述进程-PCB
系统当中可以同时存在大量进程,使用命令ps aux便可以显示系统当中存在的进程。
而当你开机的时候启动的第一个程序就是我们的操作系统(即操作系统是第一个加载到内存的),我们都知道操作系统是做管理工作的,而其中就包括了进程管理。而系统内是存在大量进程的,那么操作系统是如何对进程进行管理的呢?
这时我们就应该想到管理的六字真言:先描述,再组织。操作系统管理进程也是一样的,操作系统作为管理者是不需要直接和被管理者(进程)直接进行沟通的,当一个进程出现时,操作系统就立马对其进行描述,之后对该进程的管理实际上就是对其描述信息的管理。
进程信息被放在一个叫做进程控制块的数据结构中,可以理解为进程属性的集合,课本上称之为PCB(process control block)。
操作系统将每一个进程都进行描述,形成了一个个的进程控制块(PCB),并将这些PCB以双链表的形式组织起来。
这样一来,操作系统只要拿到这个双链表的头指针,便可以访问到所有的PCB。此后,操作系统对各个进程的管理就变成了对这条双链表的一系列操作。
例如创建一个进程实际上就是先将该进程的代码和数据加载到内存,紧接着操作系统对该进程进行描述形成对应的PCB,并将这个PCB插入到该双链表当中。而退出一个进程实际上就是先将该进程的PCB从该双链表当中删除,然后操作系统再将内存当中属于该进程的代码和数据进行释放或是置为无效。
总的来说,操作系统对进程的管理实际上就变成了对该双链表的增、删、查、改等操作。
task_struct-PCB的一种
进程控制块(Process Control Block,PCB)是操作系统中用于管理和跟踪进程信息的数据结构。每个正在运行的进程都有一个相关联的 PCB,它包含了操作系统需要了解的关于该进程的所有信息。
进程控制块(PCB)是描述进程的,在C++当中我们称之为面向对象,而在C语言当中我们称之为结构体,既然Linux操作系统是用C语言进行编写的,那么Linux当中的进程控制块必定是用结构体来实现的。
PCB实际上是对进程控制块的统称,在Linux中描述进程的结构体叫做task_struct。
task_struct是Linux内核的一种数据结构,它会被装载到RAM(内存)里并且包含进程的信息。
task_struct内容分类
task_struct 是 Linux 内核中用于表示进程的数据结构,也是 Linux 中的进程控制块(PCB)。它包含了描述进程状态和管理进程所需的各种信息。你提到的 task_struct 中的主要信息如下:
-
标识符(Identifier):用来唯一标识该进程的信息,通常由进程 ID(PID)表示,以区别其他进程。
-
状态(State):描述了进程的当前状态,如运行、就绪、阻塞等,以及与进程退出相关的信息,如退出代码和退出信号。
-
优先级(Priority):相对于其他进程的优先级,影响进程的调度顺序。
-
程序计数器(Program Counter):记录了下一条即将被执行的指令的地址。
-
内存指针(Memory Pointers):包括了指向程序代码、进程数据以及与其他进程共享的内存块的指针。
-
上下文数据(Context Data):保存了进程执行时处理器寄存器中的数据,用于在进程被调度时恢复其执行现场。
-
I/O 状态信息(I/O State Information):包括了进程发起的 I/O 请求、分配给进程的 I/O 设备以及进程正在使用的文件列表等信息。
-
记账信息(Accounting Information):记录了处理器时间总和、使用的时钟总和、时间限制等与进程执行相关的计数信息。
-
其他信息:可能还包括了与进程相关的其他信息,如进程的父进程、子进程列表等。
task_struct 在 Linux 内核中扮演着重要的角色,它是操作系统管理和调度进程的关键数据结构之一,通过对其内容的管理和更新,操作系统可以有效地管理系统中运行的所有进程。
查看进程
通过系统目录查看
在根目录下有一个名为proc的系统文件夹。
文件夹当中包含大量进程信息,其中有些子目录的目录名为数字。
这些数字其实是某一进程的PID,对应文件夹当中记录着对应进程的各种信息。
通过ps命令查看
单独使用ps命令,会显示所有进程信息。
ps命令与grep命令搭配使用,即可只显示某一进程的信息。
通过系统调用获取进程的PID和PPID
通过使用系统调用函数,getpid和getppid即可分别获取进程的PID和PPID。
我们可以通过一段代码来进行测试。
通过系统调用创建进程- fork初始
fork是一个系统调用级别的函数,其功能就是创建一个子进程。
每出现一个进程,操作系统就会为其创建PCB,fork函数创建的进程也不例外。
我们知道加载到内存当中的代码和数据是属于父进程的,那么fork函数创建的子进程的代码和数据又从何而来呢?
当使用 fork()
函数创建子进程时,子进程会复制父进程的地址空间,包括代码段、数据段、堆栈等。这种复制是通过操作系统的页表机制来实现的。具体来说,当调用 fork()
函数时,操作系统会创建一个新的进程控制块(PCB)用于子进程,并分配给子进程一个独立的进程标识符(PID)。然后,操作系统会将父进程的地址空间完整地复制到子进程的地址空间中,包括代码、数据、堆和栈等。这样,子进程就拥有了与父进程相同的代码和数据,但是它们是独立的,各自运行在自己的进程上下文中。
这种复制是通过写时复制(Copy-on-Write,COW)技术来实现的。具体来说,在 fork()
调用之后,父进程和子进程会共享相同的物理内存页。只有当父进程或子进程尝试修改共享的内存页时,操作系统才会进行实际的复制操作,以确保父进程和子进程之间的内存数据不会相互影响。这样,当父进程和子进程都只是读取共享的内存时,它们可以共享相同的物理内存页,提高了内存利用效率。
总之,fork()
函数创建的子进程的代码和数据来自于父进程的地址空间的复制,通过写时复制技术来实现内存共享和延迟复制,从而提高了系统的性能和效率。
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main() {
int data = 100;
pid_t pid;
// 使用 fork() 函数创建子进程
pid = fork();
if (pid == -1) {
// 创建子进程失败
perror("fork");
return 1;
} else if (pid == 0) {
// 子进程
printf("Child Process: PID=%d, Parent PID=%d\n", getpid(), getppid());
// 在子进程中修改变量的值
data = 200;
printf("Child Process: data=%d\n", data);
} else {
// 父进程
printf("Parent Process: PID=%d, Child PID=%d\n", getpid(), pid);
// 在父进程中打印变量的值
printf("Parent Process: data=%d\n", data);
}
return 0;
}
运行该程序,你会看到父子进程会打印出不同的进程ID,并且父子进程中的 data
变量的值也会有所不同。这是因为在调用 fork()
函数后,子进程会复制父进程的地址空间,包括变量的值,但之后父子进程的变量是独立的,各自的修改不会影响到对方。
Parent Process: PID=1234, Child PID=1235
Parent Process: data=100
Child Process: PID=1235, Parent PID=1234
Child Process: data=200
这个例子展示了父子进程共享变量的情况,但在实际的使用中,父子进程通常会在不同的工作区域内操作数据,以避免因为同时访问共享数据而导致的竞态条件和数据不一致性问题。
Linux进程状态
一个进程从创建而产生至撤销而消亡的整个生命期间,有时占有处理器执行,有时虽可运行但分不到处理器,有时虽有空闲处理器但因等待某个时间的发生而无法执行,这一切都说明进程和程序不相同,进程是活动的且有状态变化的,于是就有了进程状态这一概念。
运行状态(Running)- R
- 概念:进程正在执行并占用 CPU 时间,在操作系统的调度策略下,CPU 分配时间片给该进程执行。
- 在操作系统中的体现:在操作系统的进程调度队列中,处于运行状态的进程被调度执行,CPU 执行其指令,直到其时间片用完或者主动放弃 CPU。
一个进程处于运行状态(running),并不意味着进程一定处于运行当中,运行状态表明一个进程要么在运行中,要么在运行队列里。也就是说,可以同时存在多个R状态的进程。
小贴士: 所有处于运行状态,即可被调度的进程,都被放到运行队列当中,当操作系统需要切换进程运行时,就直接在运行队列中选取进程运行。
#include <stdio.h>
#include <unistd.h>
int main() {
printf("Running Process: PID=%d\n", getpid());
sleep(5); // 模拟进程执行
return 0;
}
该程序简单地打印出当前进程的 PID,并模拟了进程执行的过程,通过 sleep(5)
函数让进程休眠 5 秒。
在终端中打印出当前进程的 PID,然后程序会在运行状态下等待 5 秒,之后退出。
浅度睡眠状态(Light Sleep)- S
- 概念:进程处于等待某些事件的状态,但仍然可以很快地被唤醒,不会阻塞其他进程。
- 在操作系统中的体现:进程调用了一些阻塞型的系统调用,如等待 I/O 操作完成或等待信号量,进入睡眠状态,但不会阻塞其他进程的执行。当事件发生时,进程会被唤醒,转为就绪状态。
#include <stdio.h>
#include <unistd.h>
int main() {
printf("Light Sleep Process: PID=%d\n", getpid());
// 模拟等待键盘输入
char buffer[100];
fgets(buffer, sizeof(buffer), stdin);
return 0;
}
该程序打印出当前进程的 PID,并等待用户在终端输入。使用 fgets()
函数等待用户输入,此时进程处于浅度睡眠状态。
在终端中打印出当前进程的 PID,然后程序会等待用户输入。当用户输入完成并按下回车键后,程序退出。
深度睡眠状态(Deep Sleep)- D:
- 概念:进程处于等待某些事件的状态,且需要较长时间才能被唤醒,期间不占用 CPU 时间。
- 在操作系统中的体现:进程调用了一些长时间阻塞的系统调用,如等待键盘输入或者等待网络数据到达,进入深度睡眠状态。在等待的过程中,进程不会占用 CPU 时间,直到事件发生并且进程被唤醒,才转为就绪状态。
#include <stdio.h>
#include <unistd.h>
int main() {
printf("Deep Sleep Process: PID=%d\n", getpid());
// 模拟等待网络数据到达
sleep(10);
return 0;
}
该程序打印出当前进程的 PID,并通过 sleep(10)
函数模拟了等待网络数据到达的过程,此时进程处于深度睡眠状态。
在终端中打印出当前进程的 PID,然后程序会休眠 10 秒,之后退出。
暂停状态(Paused)- T
- 概念:进程暂时停止执行,不占用 CPU 时间,可以通过发送信号或其他方式来唤醒。
- 在操作系统中的体现:进程被暂停执行,不会占用 CPU 时间,不参与调度。它可能处于等待某个条件满足的状态,例如等待信号量的释放。进程可以通过收到信号或其他事件的方式被唤醒,然后重新进入就绪状态。
#include <stdio.h>
#include <unistd.h>
int main() {
printf("Paused Process: PID=%d\n", getpid());
// 进入暂停状态
pause();
printf("Resumed Process: PID=%d\n", getpid()); // 不会被执行
return 0;
}
该程序打印出当前进程的 PID,并调用 pause()
函数让进程进入暂停状态,直到接收到信号才会被唤醒。
在终端中打印出当前进程的 PID,然后程序会一直处于暂停状态,不再继续执行,直到接收到信号为止。
僵尸状态(Zombie)- Z
- 概念:进程已经结束执行,但其进程描述符仍然存在,直到父进程调用
wait()
或waitpid()
函数来获取子进程的退出状态信息后才会被清除。 - 在操作系统中的体现:当一个进程结束执行后,它的进程描述符会保留在系统中,此时进程成为僵尸进程。僵尸进程不再占用 CPU 时间,但占用系统资源。当父进程调用
wait()
或waitpid()
函数时,操作系统会清除僵尸进程的进程描述符,释放相关资源。
#include <stdio.h>
#include <unistd.h>
int main() {
pid_t pid = fork();
if (pid == -1) {
perror("fork");
return 1;
} else if (pid == 0) {
// 子进程
printf("Child Process: PID=%d\n", getpid());
return 0;
} else {
// 父进程
printf("Parent Process: PID=%d, Child PID=%d\n", getpid(), pid);
// 等待子进程结束
sleep(10);
return 0;
}
}
该程序创建一个子进程,子进程打印出自己的 PID 后立即退出,此时子进程的进程描述符会保留在系统中,成为僵尸进程。父进程等待一段时间后退出。
在终端中先打印出父进程和子进程的 PID,然后父进程会等待 10 秒,之后退出。子进程会立即退出,成为僵尸进程。可以通过 ps
命令查看僵尸进程。
死亡状态-X
死亡状态只是一个返回状态,当一个进程的退出信息被读取后,该进程所申请的资源就会立即被释放,该进程也就不存在了,所以你不会在任务列表当中看到死亡状态(dead)。
僵尸进程
一个进程若是正在等待其退出信息被读取,那么我们称该进程处于僵尸状态。而处于僵尸状态的进程,我们就称之为僵尸进程。
例如,对于以下代码,fork函数创建的子进程在打印5次信息后会退出,而父进程会一直打印信息。也就是说,子进程退出了,父进程还在运行,但父进程没有读取子进程的退出信息,那么此时子进程就进入了僵尸状态。
示例代码:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
printf("I am running...\n");
pid_t id = fork();
if(id == 0){ //child
int count = 5;
while(count){
printf("I am child...PID:%d, PPID:%d, count:%d\n", getpid(), getppid(), count);
sleep(1);
count--;
}
printf("child quit...\n");
exit(1);
}
else if(id > 0){ //father
while(1){
printf("I am father...PID:%d, PPID:%d\n", getpid(), getppid());
sleep(1);
}
}
else{ //fork error
}
return 0;
}
当子进程退出后,子进程的状态就变成了僵尸状态。
僵尸进程的危害
1.僵尸进程的退出状态必须一直维持下去,因为它要告诉其父进程相应的退出信息。可是父进程一直不读取,那么子进程也就一直处于僵尸状态。
2.僵尸进程的退出信息被保存在task_struct(PCB)中,僵尸状态一直不退出,那么PCB就一直需要进行维护。
3.若是一个父进程创建了很多子进程,但都不进行回收,那么就会造成资源浪费,因为数据结构对象本身就要占用内存。
4.僵尸进程申请的资源无法进行回收,那么僵尸进程越多,实际可用的资源就越少,也就是说,僵尸进程会导致内存泄漏。
孤儿进程
在Linux当中的进程关系大多数是父子关系,若子进程先退出而父进程没有对子进程的退出信息进行读取,那么我们称该进程为僵尸进程。但若是父进程先退出,那么将来子进程进入僵尸状态时就没有父进程对其进行处理,此时该子进程就称之为孤儿进程。
若是一直不处理孤儿进程的退出信息,那么孤儿进程就会一直占用资源,此时就会造成内存泄漏。因此,当出现孤儿进程的时候,孤儿进程会被1号init进程领养,此后当孤儿进程进入僵尸状态时就由int进程进行处理回收。
例如,对于以下代码,fork函数创建的子进程会一直打印信息,而父进程在打印5次信息后会退出,此时该子进程就变成了孤儿进程。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
printf("I am running...\n");
pid_t id = fork();
if(id == 0){ //child
int count = 5;
while(1){
printf("I am child...PID:%d, PPID:%d\n", getpid(), getppid(), count);
sleep(1);
}
}
else if(id > 0){ //father
int count = 5;
while(count){
printf("I am father...PID:%d, PPID:%d, count:%d\n", getpid(), getppid(), count);
sleep(1);
count--;
}
printf("father quit...\n");
exit(0);
}
else{ //fork error
}
return 0;
}
在父进程未退出时,子进程的PPID就是父进程的PID,而当父进程退出后,子进程的PPID就变成了1,即子进程被1号进程领养了。