【Linux】进程的概念 + 查看进程

前言:
在前面我们学习了Liunx的基本指令和权限相关知识,还有基本工具的使用,有了以上的基础知识我们本章将正式接触Linux操作系统。

1.冯诺依曼体系结构

我们常见的计算机,如笔记本。我们不常见的计算机,如服务器,大部分都遵守冯诺依曼体系。
在这里插入图片描述

  • 这里的存储器: 就是内存,不是磁盘!

  • CPU(运算器+控制器)

  • 运算器:算术运算,逻辑运算

  • 控制器:CPU是可以响应外部事件。比如:拷贝数据到内存

  • 输入设备: 键盘,话筒,摄像头,磁盘,网卡……

  • 输出设备: 显示器,音响,磁盘,网卡,显卡……

补充内容:

  1. CPU读取数据(数据+代码),都是要从内存中读取,站在数据的角度,我们认为CPU不和外设直接交互。
  2. CPU要处理数据,需要先将外设中的数据,加载到内存,站在数据的角度,外设直接只和内存打交道。

程序要运行,必须先被加载到内存中

1.1 内存存在的意义

为什么要有内存?
a. 技术角度
存储速度的差别: 寄存器的存取速度 > cpu的运算速度 > L1 ~ L3Cache(各种缓存Cache) > 内存 >> 外设(磁盘) >> 光盘磁带
b. 数据角度
外设不和CPU直接交互,而是和内存交互,CPU也是如此
c. 成本角度
造价: 寄存器 >> 内存 >> 磁盘(外设)

如果冯诺依曼体系结构中没有内存的话:

  • 那么整个体系的效率是很低下,因为是由最慢的设备决定的。

  • 计算CPU速度够快,但是还是要等外设,这既是著名的木桶原理。

  • 内存的意义:

有存储器的存在,让软件的存在具有了更大的意义,开机的时候,就是将操作系统加载到存储器当中
内存对应的最大意义:使用较低的钱的成本,能够获得较高的性能

  • 补充:
  • 中央处理器CPU也会和外设有交互,协调数据流向。
  • 中央处理器CPU只是个具有运算和控制能力的体现木偶,真正让中央处理器去完成计算和某些控制的是整个计算机的大脑,叫做软件,最具有代表性的就是操作系统,是操作系统来控制CPU的。

1.2 程序加载到内存的含义

  • 在我们之前学习编程语言例如:C/C++时,我们都听过这样一句话:编译好之后的软件/程序,要运行,必须先加载到内存
  • 为什么呢?
    答案就是由体系结构(冯·诺依曼体系结构)决定的
    具体解释:

C/C+编译好的程序就必须从磁盘加载到内存要让CPU能够读取。
我们编译好的程序是个文件是在磁盘上(外设),CPU读取数据(数据+代码),都是要从内存中读取,所以也就要求要运行程序,就必须将程序先加载到内存,因为CPU只会从内存当中读取指令代码和数据。

1.3 程序的预加载:

  • 几乎所有的硬件,只能被动的完成某种功能,不能主动的完成某种功能,一般都是要配合软件完成的。
  • 开机等待的本质,就是将操作系统加载到内存当中,因为体系结构规定,CPU要执行代码,执行的可不仅仅是我们写的代码,还有操作系统的代码,所以必须先把操作系统加载到内存,这就是预加载。
  • 操作系统一旦被加载之后,在软件层面上,就可以预先把将来要访问的数据或文件,可以提前加载到内存中。

数据在流动的时候------》输入到内存---------》从内存到CPU--------》CPU计算处理完---------------》将结果写回内存,然后定期再刷新到外设。

补充:

内存的存在可以去适配外设和CPU之间速度不匹配的问题,因为内存的存在可以去预先装载一些常见的内存管理软件,数据管理软件。

2 .认识进程

2 .1 如何理解管理

  • 管理的本质是:对数据的管理
    管理的本质:不是对被管理对象进行直接管理,而是只要拿到被管理对象的所有的相关数据,我们对数据的管理,就可以体现对对象的管理。
  • 管理的核心理念:先描述,再组织
    用C语言或C++描述,用数组结构组织数据。管理的本质是对数据做管理—对某种数据结构的管理--------》对数据结构的各种操作,增删查改。

重点:操作系统是一款软件,是一款专门搞管理的软件,软件可以管理软件,就像人可以管理人一样

2 .2 什么叫是进程:(初步理解)

  • 进程是一个运行起来的程序。
  • 程序是个文件,是存在磁盘上的,不能简单的认为,将程序从磁盘加载到内存,这个程序就是进程。
  • 操作系统里面,可能同时存在大量的进程
  • 对进程的管理,是先描述,再组织

进程=对应的代码和数据 +进程对应的PCB结构体。

2 .2.1 PCB
  • 描述起来,用struct结构体
  • 组织起来,用链表或其他高效的数据结构
  • 而Linux中的task_struct是一款具体的PCB
    在这里插入图片描述

有大量的进程就必须把进程先描述再组织起来,把进程组织起来实际上是把描述进程的进程控制块组织起来。

task_ struct内容分类

  • 标示符: 描述本进程的唯一标示符,用来区别其他进程。

  • 状态: 任务状态,退出代码,退出信号等。

  • 优先级: 相对于其他进程的优先级。

  • 程序计数器: 程序中即将被执行的下一条指令的地址。

  • 内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针。(mm_struct–> 虚拟地址空间

  • 上下文数据: 进程执行时处理器的寄存器中的数据[休学例子,要加图CPU,寄存器]。

  • I/O状态信息:包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。(文件信息

  • 记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。

  • 其他信息

那我们学习进程到底学的是什么呢?

  • 我们学习的是进程控制块里面有什么属性!

第一阶段对进程的理解总结:

  • 当一个程序从磁盘加载到内存,将代码和数据加载到内存只是第一步,第二步,操作系统为了管理这个进程,需要为该进程创建对应的描述该进程的进程控制块PCB,Liunx下叫task struct。
  • 只要在内存当中被操作系统管理,操作系统实际管的根本不是代码和数据,而是管的则是进程的PCB结构体。
  • 第一阶段进程的理解:程序加载到内存之后的代码和数据,以及操作系统为了管理进程,所生成的描述进程的进程控制块PCB结构体(内核数据结构 +代码和数据,这二者合起来,叫做进程)。
  • 一个进程有一个PCB描述起来了,系统中有大量的PCB,只需要将系统中的PCB用数据结构组织起来,对应的对进程的管理就变成了对数据结构的增删查改。

2.3 简单认识操作系统

在这里插入图片描述

2.3 .1 操作系统如何提供服务

计算机和OS设计出来就是为了给人们服务的,那么是如何给我们提供服务的呢?

  • 所有的应用程序都没有资格直接访问硬件,因为硬件的管理者是操作系统
  • 操作系统是不相信任何人的!不会直接暴露自己的任何数据结构,代码逻辑,其他数据相关的细节!
  • 操作系统是通过给用户提供接口的方式为用户提供服务的~
  • Linux操作系统是用C语言写的,这里所谓的“接口”,本质就是C函数
  • 学习系统编程本质就是在学习这里的系统接口。

补充:
一门语言跨平台可移植,在Windows和Linux下都能选择其对应的接口,上层提供的都是printf(),原因标准库中用了多态,同个接口在不同的平台下实现同一个或者不同的功能

2.4 查看进程

  • 我们自己写的代码,编译成为可执行程序,启动之后就是一个进程。
  • 别人写的程序,启动之后也是进程。
2. 4 .1 第一种查看进程的方式:

在这里插入图片描述
a:all
j:jobs
x:以特定格式显示

显示出前五行:
在这里插入图片描述
Windows下是通过双击打开一个进程, 而Linux则是通过./启动,在系统中找到可执行程序启动。

2. 4 .2 第二种查看进程的方式:

Linux的根目录下有个 proc目录里面放的就是实时的进程:
在这里插入图片描述
proc:内存文件系统,里面放的是当前系统实时的进程信息。

  • 要想获得PID为26746的进程信息,你需要查看 /proc/26746这个文件夹。
  • 在proc中打开这个进程,可以查看它的详细属性
    在这里插入图片描述

2.5 对进程的当前工作路径的理解

我们之前学习C / C++语言的时候,我们就只是肤浅的理解,当前工作路径就是源文件或程序所在的路径。

事实上,并不是。

  • 我们使用ls -al选项来在 proc目录中对应的进程中看一下它的更详细的属性:

在这里插入图片描述

  • 当前路径并不是源文件所在的路径,而是运行进程时所处的路径。
  • 换句话说,就是Bin运行时所处的路径。

3 .进程的系统调用

3 .1 父进程与子进程

  • 父进程:指已创建一个或多个子进程的进程。常使用**Fork()**来创建多个子进程。
  • 通过getpid函数来获取当前进程的ID,也可以通过getppid来获取父进程的ID
    在这里插入图片描述
    代码演示:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>

int main()
{
    while(1)
    {
        printf("I am a process! pid: %d ppid: %d\n",getpid(), getppid());
        sleep(1);
    }

    return 0;
}
  • 每次执行一个可执行程序之后,进程的ID都会改变,上图也验证了这一点,但是我们惊奇的发现,为啥父进程的ID始终都是一个值,一直都是不变的呢?
    在这里插入图片描述
    几乎我们在命令行上所执行的所有的指令(你的cmd),都是bash进程的子进程

衍生问题:

  • bash怎么创建的子进程
  • bash怎么让子进程执行我的程序
  • bash的父进程又是谁

3 .2 fork函数创建子进程

  • fork函数是用来创建子进程的,它有两个返回值。
  • 子进程代码共享,数据各自开辟空间,私有一份
    在这里插入图片描述

3 .2 .1 fork函数的返回值:

在这里插入图片描述

  • 成功的话:将子进程的pid返回给父进程,0被返回给子进程。
  • 失败的话:-1直接返回给父进程,没有子进程没创建。

代码演示:

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>

int main()
{
    pid_t id  = fork();

    printf("Hello World!  id = %d\n", id);

    return 0;
}
  • 一条打印语句竟然有两个打印结果,因为Fork之后产生新的进程。
    fork 之后通常要用 if 进行分流
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>

int main()
{
    pid_t id = fork();
    //id == 0 : 子进程 , id > 0 : 父进程
    
    if(id == 0)
    {
        while(1)
        {
            printf("我是子进程,我的pid:%d,我的父进程是:%d\n", getpid(), getppid());
            sleep(1);
        }
    }
    else  
    {
        while(1)
        {
            printf("我是父进程,我的pid:%d,我的父进程是:%d\n", getpid(), getppid());
            sleep(1);
        }
    }

    return 0;
}

3 .2 .2 fork函数两个返回值的原因:

如何解释呢?fork如何做到会有不同的返回值?

答:fork之后,OS做了什么?是不是系统多了一个进程
父进程是:task_struct + 数据和代码
子进程也是:task_struct + 子进程的数据和代码

  • 子进程的task_ stuct对象内部的数据基本是从父进程继承下来的。
  • 子进程和父进程共享代码,fork之后,父子进程执行同样的代码。
  • 父进程return一次,子进程return一次,不就是两次返回吗
  • Fork之后,父进程和子进程返回值不同,可以通过不同的返回值,判断让父子执行不同的代码块

补充:

  • 进程是由task_struct 和 对应的数据和代码组成。
  • 那么我们平时用的指令的执行后,它的进程对应的代码在哪呢?

以ls为例: ls变成进程之后,该进程的代码就是从磁盘/usr/bin/ls路径下读取数据代码。

  • 父子进程被创建出来,哪一个进程先运行呢??

不一定!! 谁先运行,不一定,这个是由操作系统的调度器决定的!!
操作系统和CPU运行某一个进程,本质从task_struct 形成的队列中挑选一个task_struct,来执行它的代码。

4 .进程的状态

操作系统就像是计算机里的哲学一样,因为操作系统这门学科讲的范围很宽泛,它的理论内容适用于各个操作系统,而我们要具体的学习某一款操作系统,那就是Linux

凡是说进程,就必须先想到进程的task_ struct。

  • 进程状态本质上是个uint8 整数,整数在进程的task struct中
  • task_ struct中会包含进程的相关的信息。
    在这里插入图片描述
    在这里插入图片描述

4 .1 运行状态(R)

  • 运行状态 是进程在CPU上运行,就叫运行态吗?

答:不是的,操作系统当中每个CPU,都会创建一个runqueue,所以一个进程想被调动,说白了就是将自己的进程放到运行队列当中。

进程只要在运行队列中叫做运行态,不代表正在运行,代表我已经准备好了,随时可以调度!
在这里插入图片描述

4 .2 死亡状态(X dead)

死亡状态又叫做终止状态:这个状态只是一个返回状态,你不会在任务列表里看到这个状态。

  • 进程终止状态:进程已完成执行,
  • 终止就是最终资源释放,但是PCB控制块至少还在,要让操作系统来释放
  • X状态,瞬时性非常强。

4 .3 阻塞状态(s)

  • 进程运行可能需要申请更多的资源:磁盘,网卡,显示器资源,声卡/音响等
    -我们申请资源时,如果该资源暂时没有准备好,当前进程要从runqueue中移除。
    重点:
  • 当我们的进程此时在等待外部资源的时候,该进程的代码,不会被执!!
  • 我的进程卡住了 — 进程阻塞!上层看来就是某些任务卡住了。

补充知识点

  • CPU运行的速度非常快,但是运行队列的周转周期非常短,看起来所有进程都在运行。
  • 单核CPU在任意时间点都只能运行一个进程。

在这里插入图片描述
疑问:为什么我们一直向显示器打印,但却是S状态?

  • 因为CPU足够快,外设又很慢,CPU速度的速度是远大于外设的
  • 所以大部分时间都是S状态,都是睡眠阻塞状态

虽然一直在刷屏,但是还是S状态,原因也是在printf上,不断地往显示器上打印,但是显示器是个外设速度非常慢,即便是闲着准备好被刷新也需要花费时间。这个进程90%的情况都是在等,在等显示器就绪。
只有光是一个死循环,不调用外设的时候才是一直处于R状态。

4 .4 深度睡眠状态(D)

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

假设场景:

当一个进程向磁盘写文件的时候,由于要写的文件很大,所以进程要在那里等,如果等的时间太长了的话,操作系统见到一个进程在那里很悠闲直接把它干掉了,那等磁盘将文件写完之后,回头一看,傻眼了,进程不见了,那写入的文件怎么处理呢??如果该文件写入失败了,结果返回的时候发现进程不见了,那么数据就丢了,后果很严重。
尽然进程要等,就是要等一个返回值,就是为了判断文件写成功了没!!

所以这个进程不能随便杀掉,所以操作系统就将该进程设置成了D状态

  • 凡是D状态的进程,操作系统无权杀掉该进程,只能等该进程自己醒来。
  • D状态的进程操作系统没权利将其杀掉,只能通过关机重启 or 拔掉电源的方式来强制杀掉该进程。
  • 如果一个系统当中存在大量的D状态进程,关机都关不掉
  • 一般而言,linux中,如果我们等待的是磁盘资源,我们进程阻塞所处的状态就是D
    不过这种情况不多见,很难能见到~

补充:

有时候闪退的问题,是服务器压力过大,OS是会终止用户进程的!

4 .5 僵尸状态(Z)

  • 一个进程已经退出,但是还不允许被OS释放处于一个被检测的状态----僵尸状态。
  • 维持僵尸状态,是为了让该进程被操作系统或父进程回收。
  • 当一个linux中的进程退出的时候,一般不会直接进入X(终止)状态,而是进入Z状态。

我们不禁发出疑问,为什么?

  • 首先我们知道,子进程被创建出来,一定是要执行任务的
  • 当子进程退出的时候,一般需要将进程的执行结果,告知到父进程os.

进程Z,就是为了维护退出信息,可以让父进程或者os读取的。
僵尸进程存在的意义,就是说明这个进程退出的时候,是因为什么原因退出的。

4 .5.1 模拟僵尸进程

如果创建子进程,子进程退出了,父进程不退出,也不等待子进程,子进程退出之后所处的状态就是Z状态。(ps:所谓等待子进程就是回收)

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h> 

int main()
{
    pid_t id = fork();
    
    if(id == 0)
    {
        //child
        int cnt = 5;
        while(cnt)
        {
            printf("我是子进程,我剩下 %d S\n", cnt);
            cnt--;
            sleep(1);
        }
        printf("我是子进程,我已经僵尸了,等待被检测\n");
        exit(0);
    }
    else 
    {
        while(1)
        {
            sleep(1);
        }
    }

    return 0;
}

子进程5秒后退出,父进程一直在跑,也不回收子进程,我们代码没有写回收,所以子进程就是没有回收

4 .5.2 长时间僵尸的危害

  • 如果没有人回收子进程的僵尸,该状态会一直维护!该进程的相关资源(task_struct) 不会被释放!一个很严重的问题那就是 — 内存泄漏
  • 什么情况会一直僵尸?

父进程不回收它,会一直僵尸状态。

  • 一个进程僵尸了,是不可被杀死的。都已经成僵尸了还怎么杀死

1.使用wait()或waitpid():父进程可以调用wait()或waitpid()函数来等待子进程终止,并获取它的终止状态
此方法父进程会被挂起

#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
 
int main() {
    pid_t pid = fork();
    if (pid == 0) {
        // 子进程代码
        _exit(0);
    } else if (pid > 0) {
        // 父进程代码
        wait(NULL); // 等待子进程终止
    } else {
        // fork失败
        perror("fork");
    }
    return 0;
}

2.父进程忽略SIGCHLD信号:如果不想让子进程编程僵尸进程,可在父进程中加入:signal(SIGCHLD,SIG_IGN);
如果将此信号的处理方式设为忽略,可让内核把僵尸子进程转交给init进程去处理

signal(SIGCHLD,SIG_IGN);

3.注册SIGCHLD信号处理函数: 父进程可以通过捕获SIGCHLD信号来处理子进程终止事件,并调用waitpid()来避免僵尸进程的产生

#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <signal.h>
#include <stdio.h>
 
void sigchld_handler(int signum) {
    // 使用非阻塞方式调用waitpid(),防止阻塞父进程
    while (waitpid(-1, NULL, WNOHANG) > 0);
}
 
int main() {
    struct sigaction sa;
    sa.sa_handler = sigchld_handler;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = SA_RESTART;
    sigaction(SIGCHLD, &sa, NULL);
 
    pid_t pid = fork();
    if (pid == 0) {
        // 子进程代码
        _exit(0);
    } else if (pid > 0) {
        // 父进程代码
        sleep(10); // 模拟父进程的工作
    } else {
        // fork失败
        perror("fork");
    }
    return 0;
}

4.daemon进程: 将父进程设计成守护(后台)进程(daemon),这样孤儿进程会被init进程(PID 1)领养,而init进程会自动回收孤儿进程,避免僵尸进程的产生。

状态后面有S+的,代表这个进程是个前台进程,能在键盘ctr/ + C的是前台进程; 后台进程ctr/ + C干不掉。

孤儿进程没有那个S+,它是个后台进程。

4 .6 暂停状态(T/t)

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

在这里插入图片描述

  • 暂停状态主要用于调试场景中。

5 .进程状态的整体总结

  • 系统中一定是存在各种资源的(不仅仅是CPU)网卡,磁盘,显卡都是资源;所以,系统中不只是只存在一种队列

在这里插入图片描述

  • 理解操作系统中的进程状态:
    a.新建
    b.运行:task_struct 结构体在运行,队列中排队,就叫做运行态
    c.阻塞:等待非CPU资源就绪,阻塞状态
    d.挂起:当内存不足的时候,Os通过适当的置换进程的代码和数据到磁盘,进程的状态就叫做挂起!
    在这里插入图片描述
    尾声
    看到这里,相信大家对这个C++有了解了。
    如果你感觉这篇博客对你有帮助,不要忘了一键三连哦

相关推荐

  1. Linux进程基本概念

    2024-07-11 10:12:04       20 阅读

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2024-07-11 10:12:04       7 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2024-07-11 10:12:04       8 阅读
  3. 在Django里面运行非项目文件

    2024-07-11 10:12:04       7 阅读
  4. Python语言-面向对象

    2024-07-11 10:12:04       10 阅读

热门阅读

  1. 0124__Linux和Unix的Access.conf安全配置

    2024-07-11 10:12:04       6 阅读
  2. Android11 应用启动流程

    2024-07-11 10:12:04       11 阅读
  3. CentOS-6的iso下载地址镜像yum源

    2024-07-11 10:12:04       9 阅读
  4. 什么是CLR

    2024-07-11 10:12:04       9 阅读
  5. 获取线程id

    2024-07-11 10:12:04       9 阅读
  6. 小抄 20240709

    2024-07-11 10:12:04       7 阅读
  7. 24/07/10数据结构(5.1213)链表OJ

    2024-07-11 10:12:04       9 阅读
  8. VUE学习列表

    2024-07-11 10:12:04       10 阅读
  9. 手动安装Ruby 1.9.3并升级RubyGems

    2024-07-11 10:12:04       14 阅读
  10. uniapp APP端解决video seek跳转时间不准确的问题

    2024-07-11 10:12:04       8 阅读