【Linux】进程管理:从理论到实践(一)

Zfox_ 2024-10-03 09:07:11 阅读 57

🌈 个人主页:Zfox_

🔥 系列专栏:Linux

目录

一: 🔥 进程的基本概念 二: 🔥 描述进程-PCB三: 🔥 查看进程 🥝 通过系统目录🥝 通过ps命令

四: 🔥 创建进程-fork() 📚 五: 🔥 进程状态🥝 进程状态查看🍊 进程状态转化

六: 🔥 共勉

一: 🔥 进程的基本概念

在给进程下定义之前,我们先了解一下进程:

💦 我们在编写完代码并运行起来时,在我们的磁盘中会形成一个可执行文件,当我们双击这个可执行文件时(程序时),这个程序会加载到内存中,而这个时候我们不能把它叫做程序了,应该叫做进程。

💦 所以说,只要把程序(运行起来)加载到内存中,就称之为进程。

💦 进程的概念:程序的一个执行实例,正在执行的程序等。

💦 如果站在内核的角度来看:进程是分配系统资源的单位。

二: 🔥 描述进程-PCB

🍊 一个概念需要一个具体的结构体来进行描述的。进程中的信息就被放在了一个叫做进程控制块(PCB)的结构体中。

<code>PCB:进程控制块(结构体)

当一个程序加载到内存中,操作系统要为刚刚加载到内存的程序创建一个结构体(PCB),进程信息被放在这个结构体中(PCB),可以理解为PCB是进程的属性的集合。

在Linux操作系统下的PCB是: task_struct

🍊 task_struct 是Linux内核的一种数据结构,它会被装载到 RAM(内存) 里并且包含着进程的信息,在进程执行时,任意时间内,进程对应的 PCB 都要包含以下内容:

标示符:描述本进程的唯一标示符,用来区别其他进程状态:任务状态优先级:相对于其他进程的优先级程序计数器:程序中即将被执行的下一条指令的地址内存指针:包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块指针上下文数据:进程执行时处理器的寄存器中的数据I/O状态信息:包括显示的I/O请求,分配给进程的I/O设备和被进程使用的- - - 文件列表记账信息:可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等其他信息:…

三: 🔥 查看进程

🥝 通过系统目录

🍊 第一种方式:在 /proc 这个目录下保存着所有进程的信息。

⚡ 注意:/proc不是磁盘级别的文件

在这里插入图片描述

🥝 通过ps命令

<code>ps aux # 查看系统中所有的进程信息

ps axj # 可以查看进程的父进程号

⚡ 查看对应进程信息

ps axj | head -1 && ps axj | grep myexe

在这里插入图片描述

四: 🔥 创建进程-fork() 📚

🍊 创建进程有两种创建方式:

使用 ./ 运行某一个可执行程序,这种是最常见的方式

使用系统调用接口创建进程,即使用fork()

当时用 <code>fork() 函数之后,就在原来的进程中创建了一个子进程,在 fork() 之前的代码只被父进程执行,在 fork() 之后的代码有父子进程一起执行。

创建的子进程和父进程几乎一模一样,子进程和父进程的共享地址空间,子进程可以或者父进程中所有的文件,只有 PID 是父子进程最大的不同。

💢 下面是利用fork创建一个进程使用到的代码:

#include <iostream>

#include <vector>

#include <sys/types.h>

#include <unistd.h>

using namespace std;

const int num = 10;

void SubProcessRun()

{

while(true)

{

cout << "T am sub process, pid: " << getpid() << " , ppid" << getppid() << endl;

sleep(1);

}

}

int main()

{

vector<pid_t> allchild;

for(int i = 0; i < num; i++)

{

pid_t id = fork();

if(id == 0)

{

// 子进程

SubProcessRun();

}

allchild.push_back(id);

}

cout << "我的所有孩子是:";

for(auto child : allchild)

{

cout << child;

}

cout << endl;

while(true)

{

cout << "我是父进程,pid:" << getpid() << endl;

sleep(1);

}

return 0;

}

以下是运行结果:

在这里插入图片描述

<code>如果fork成功创建了一个进程,那么上面的代码就会输出

T am sub process, pid: 27894 , ppid27891

我的所有孩子是:27892 27893 27894 27895 27896 27897 27898 27899 27900 27901

这里面有很多有意思的点:

fork函数调用一次,返回两次。

🎯 上面的代码是如何实现执行两个不同的分支语句的呢?其实是因为fork函数会返回两个返回值,一个是子进程会返回0,一个是父进程会返回子进程的PID。所以会同时进程两个分支语句中。

并发执行

🎯 父子进程是两个并发运行的独立程序。并发(同一个cpu执行),就是两个执行流在执行的时间上有重叠的部分。也就是说父子进程谁先被调度是不能确定的。

相同但是独立的地址空间

🎯 两个进程其实地址空间是一样的,但是它们都有自己私有的地址空间,所以父子进程的运行都是独立的,一个进程中的内存不会影响另一个进程中的内存。

共享文件

🎯 子进程继承了父进程所有打开的文件,所以父进程调用fork的时候,stdout文件呢是打开的,所以子进程中执行的内容也可以输出到屏幕上。

五: 🔥 进程状态

🥝 为了弄明白正在运行的进程是什么意思,我们需要知道进程的不同状态。一个进程可以有几个状态(在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): 并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列里。

🎯 模拟实现:

可以运行任意一个可运行的程序,即可出现R状态。

S睡眠状态(sleeping) : 意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡眠(interruptible sleep))。

这种状态是一种浅度睡眠,此时的进程是在被阻塞的状态中,等待着条件的满足过后进程才可以运行。在这种状态下可以被信号激活,也可以被信号杀死。

模拟实现:

可以使用sleep() 系统调用接口使得一个进程睡眠

#include <stdio.h>

int main()

{

while (1)

{

printf("S睡眠状态\n");

sleep(100); // 睡眠100秒

}

return 0;

}

D磁盘休眠状态(Disk sleep)有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个状态的进程通常会等待IO的结束。

🎯 模拟实现:

这种情况没法模拟,一般都是一个进程正在对IO这样的外设写入或者读取的时候,为了防止操作系统不小心杀掉这个进程,所以特地创建出一个状态保护这种进程。

T停止状态(stopped)可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行。

🎯 模拟实现:

可以使用信号

kill -SIGSTOP PID // 停止进程

kill -SIGSONT PID // 继续进程

X死亡状态(dead)这个状态只是一个返回状态,你不会在任务列表里看到这个状态。进程停止执行,进程不能投入运行。通常这种状态发生在接受到SIGSTOP、SIGTSTP、SIGTTIN、SIGOUT等信号的时候。

🎯 模拟实现:

可以使用 kill -9 PID即可杀死一个进程

Z僵死状态

后面会详细讲解

孤儿进程如果父进程比子进程先退出,那么此时子进程就叫做孤儿进程。而操作系统不会让这个子进程孤苦伶仃的运行在操作系统中,所以此时孤儿进程会被 init 进程(也就是1号进程,即所有进程的祖先)领养,从此以后孤儿进程的状态和最后的PCB空间释放都是由init进程负责了。

🎯 模拟实现:

模拟实现让父进程比子进程提前退出即可

#include <stdio.h>

#include <unistd.h>

#include <sys/type.h>

int main()

{

pid_t pid = fork();

if (pid == 0) { // 子进程一直执行

while (1) {

printf("I am a child, pid=%d, ppid=%d\n", getpid(), getppid());

sleep(1);

}

} else {

int count = 3; // 父进程执行3次

while (count --) {

printf("I am a father, pid=%d, ppid=%d\n", getpid(), getppid());

sleep(1);

}

}

return 0;

}

🥝 进程状态查看

ps aux / ps axj 命令

# 每隔一秒显示进程的信息

while :; do ps axj | head -1 && ps axj | grep code | grep -v grep; sleep 1; done

在这里插入图片描述

<code>父进程退出后,自己子进程被1号init进程收养。

僵尸进程

为什么会出现僵尸进程?

💦 前面说过进程的作用是为了给操作系统提供信息的,所以在进程调用结束之后,应该将该进程完成的任务情况汇报(eixt code)给操作系统,但是进程在执行完之后已经结束了,所以此时进程的状态就是僵尸状态。

僵尸进程的概念

💦僵尸进程:即进程已经结束了,但是父进程没有使用wait()系统调用,此时父进程不能读取到子进程退出返回的信息,此时就该进程就进入僵死状态。

僵尸进程的危害

💦 进程已经结束了,但是进程控制块PCB却还是没有被释放,这时就会浪费这一块资源空间。所以会导致操作系统的内存泄漏。

如何消灭僵尸进程?

💦 僵死状态需要父进程发出wait()系统调用终止进程,如果父进程不终止进程,那么此时要消灭僵尸进程只能通过找到僵尸进程的父进程,然后kill掉这个父进程,然后僵尸进程就会成为孤儿进程,此时由init进程领养这个进程然后杀死这个僵尸进程。

🎯 模拟实现:

模拟实现让子进程比父进程提前退出。

#include <stdio.h>

#include <unistd.h>

#include <sys/type.h>

int main()

{

pid_t pid = fork();

if (pid == 0) {

int count = 3; // 子进程执行3次

while (count --) {

printf("I am a child, pid=%d, ppid=%d\n", getpid(), getppid());

sleep(1);

}

} else { // 父进程一直执行

while (1) {

printf("I am a father, pid=%d, ppid=%d\n", getpid(), getppid());

sleep(1);

}

}

return 0;

}

💦 使用shell脚本监控

# 每隔一秒显示进程的信息

while :; do ps axj | head -1 && ps axj | grep code | grep -v grep; sleep 1; done

在这里插入图片描述

如上图:子进程执行了3次之后,编程僵尸状态

🍊 进程状态转化

在这里插入图片描述

六: 🔥 共勉

以上就是我对 <code>【Linux】进程管理 的理解,会立刻更新下一篇的,觉得这篇博客对你有帮助的,可以点赞收藏关注支持一波~😉

在这里插入图片描述



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。