【Linux】深入理解Linux的进程(一)

【Linux】深入理解Linux的进程(一)

学校OS书本讲的Linux的进程太哲学。本文将解释子进程,根据指令、代码、Linux内核源码剖析Linux的进程状态,包你听懂。

另外一提,本文命令都在云服务器上运行,先前我在WSL环境下进行测试,发现有部分指令和在传统Linux运行结果不同,比如孤儿进程的处理。(还是很坑人的这方面,所以我也推荐大家现在云服务器,或者本机虚拟机、物理机上进行学习)

冯诺依曼结构

在开始前,先讲解以下冯诺依曼的计算机结构,冯诺依曼结构十分伟大,走出了计算机进入家家户户的重要一步,另外冯诺依曼结构有助于理解CPU和内存、外设之间的关系,所以这里也会讲解一遍。

一个计算机是怎么接收数据然后进行处理、发送出去的呢?

外部信息先通过输入设备把数据传入到内存中,然后由CPU对数据进行解密、处理,再把数据加密后交给内存,内存把数据交给输出设备,输出设备把数据传到网络或者显示器等地方。

冯诺依曼结构十分厉害的一点就是把内存作为核心,都和内存打交道,而不是直接把信息交给CPU,内存成为CPU的一个缓冲区。

进程描述

学校讲的PCB完成进程描述功能,供CPU找到进程地址,进行进程调度,(我这里会重新讲,你不会也无所谓),本文将其具象化。

​ 在进入主题前,我们可以思考一下------如果对一个图书馆的书籍进行管理,应该怎么管理保障不会乱。

​ 我们可以赋予书籍不同的属性,比如:一本书可以按题材分类,也可以按主旨分类。然后把相同属性的书籍放在一起。放在计算机里,我们就需要用到struct对每本书的属性进行记录,然后在管理的时候,我们可以用到数据结构与算法的知识了。你可以顺序表,可以链表,可以哈希表,可以选择最适合完成业务需求的数据结构完成管理。

​ 上述步骤就是先描述 (指创建结构体对属性进行记录的过程),再组织(将结构体封装到数据机构中,完成管理的过程)。

​ 事实上,你看到这里就已经掌握了计算机管理大部分东西的方法了。我们开始举例说明------你的计算机能够检测到你外设的状态,这就用到了先描述再组织,操作系统把不同的外设封装成不同的struct,然后进行管理。下文提到的进程也是如此。

PCB------task_struct

​ 我们看一下Linux的进程描述字段部分源码:

c 复制代码
struct task_struct {
	volatile long state;	/* -1 unrunnable, 0 runnable, >0 stopped */
	void *stack;
	atomic_t usage;
	unsigned int flags;	/* per process flags, defined below */
	unsigned int ptrace;

	int lock_depth;		/* BKL lock depth */
    
	struct list_head tasks;
	struct plist_node pushable_tasks;

	struct mm_struct *mm, *active_mm;
    pid_t pid;
	pid_t tgid;
	struct list_head ptraced;
	struct list_head ptrace_entry;
};

通过源码,我们可以知道所谓的PCB(进程属性描述字段)在Linux下就是task_struct,这个结构体描述了进程的所在文件位置、虚拟地址空间分配、文件打开的描述等等关键信息。注意struct list_head ptraced;,这个list_head本质就是封装了两个指针的结构体,而task_struct有很多list_head。他有什么用呢?

Linux的task_struct有很多个指针,这样可以把一个task_struct放在不同的数据结构、不同的链表、队列里了。而通过偏移量就可以准确找到想要找的元素位置------>&((task_struct*)0->link1)找到link1的偏移量。(struct task_struct*)((char*)next - x)找到下一个task_struct起始位置地址。这样,在不同数据结构下,都可以完美找到下一个/上一个进程描述字段在哪了。

加载到内存

我们写程序的过程,是向磁盘写内容的过程,那么怎么运行我们写的程序呢?结合冯诺依曼结构,CPU不能直接和磁盘打交道,所以你想要运行,需要先把你的进程放到内容中,这一过程我们叫做加载。

一个完整的进程 = task_struct + 自己的代码和数据。这个代码和数据就是在内存中。

Linux下一切皆文件,我们可以通过以下步骤深入理解代码加载到内存的过程

首先写一个死循环的程序,方便我们查看进程的状态,然后完成编译、链接、运行。(我这里编译链接生成myprocess文件)Linux操作系统给进程命名了一个id(我们把这个id叫做进程id,或者pid(process id)),并且生成这个id相同的文件夹放在/proc目录下,所以我们要先查看以下这个id是什么:我们新开一个会话,运行ps axj | head -1 ; ps axj | grep myprocess查看我们运行的程序信息,找到pid,这就是我们说的id。然后在/proc找相应的文件夹。详细操作如下:

可以在你的程序中通过chdir()系统调用修改cwd,这里就不演示了。

需要注意的内容

  • 那么怎么删除你刚刚运行的死循环的程序呢?你可以在刚刚运行的会话里输入ctrl c发送信号给进程,让他终止,kill -9 + pid也是一样的效果。不同的是,ctrl c只能给当前会话的在前台运行的进程发送终止信号,而kill -9可以给其他会话、后台的进程发送终止信号。信号部分会在后面详细讲。
  • /proc是内存级的,文件的属性和内容都放在内存,但是对于路径查找,还是走的文件系统那一套(文件系统会出详细解析的文章)。

子进程

子进程指在当前进程下创建的新进程,相应的,对于子进程,这个进程就是父进程。

fork

  • fork()命令可以创建进程,返回pid_t(long的宏定义),给父进程返回子进程的pid,给子进程返回0,如果创建进程失败会返回-1。
  • 子进程和父进程共享代码和数据,当子进程对数据进行修改,会发生写时拷贝。以方便节省内存空间,另一方面防止子父进程相互影响。(写时拷贝:指再内存上新开辟空间,同时完成页表映射的更新)
  • 那么问题来了
    • 为什么给父进程一个子进程pid,给子进程一个0,两个返回值不一样
      • 通过getppid()函数,子进程可以访问到父进程的pid,但是父进程不能通过函数查询到子进程的pid,如此返回值方便进行管理。
    • fork()创建子进程过程中完成了什么一系列操作?
      • 向操作系统申请新的task_struct空间
      • 父进程把自己task_struct原封不动copy给子进程
      • 把子进程task_struct放入runqueue
    • fork()为什么可以返回两次??
      • 完成创建子进程过程后,开始给return id;此时return id;会在父进程执行一遍,在子进程执行一遍,子进程还会发生写时拷贝,自然就可以返回两个值了。
    • 子父进程有什么关系?
      • 子进程共享父进程的代码和数据,当子进程对代码和数据有修改,会发生写时拷贝。
      • 子进程在结束时,会把return结果返回给父进程,tash_struct结构体也交给父进程回收。

进程状态

在此之前我们向讲三个系统调用,pid_t getpid()和pid_t getppid(),这两个系统调用分别是查看当前进程、父进程(父进程后文会讲)的进程id。先要查看运行的进程的进程状态还需要ps axj | head -1 ; ps axj | grep myprocess命令,你可以用while封装一下:while :; do ps axj | head -1; ps axj | grep myprocess; sleep 1; done就可以每过一秒查看一次myprocess进程的状态

使用ps axj指令查看Linux的进程状态 有以下几种:(守护进程在网络通信部分讲,守护进程也是一种进程状态)

R(Running)

  • 运行态:在使用ps命令查看while(1){}程序时,可以发现查到的程序大多数在R状态。
  • 内核上:进程task_struct处在runqueue中等待被CPU调用,或者已经被CPU调用。关于runqueue、进程调度算法后文会详细讲解。

S(sleep可中断睡眠)

  • 可中断睡眠:阻塞导致的休眠,因为一个设备只能由一个进程使用,在此设备交给其他进程使用的时候,这个进程就会进入阻塞休眠的状态。除了这种情况,wait等待其他进程的信号也是S状态。
  • 内核上:我们上文提到过,Linux操作系统把不同设备封装成struct对象,那么,把需要这个设备的进程放到这个设备的struct结构体中,这就是阻塞。当设备空闲,要分配给其他进程时,从这个struct中寻找下一个分配的进程即可。

D(不可中断睡眠)

  • 不可中断睡眠:不同于S状态,这个状态通常用于磁盘IO,比如你在关机的时候,有部分进程正在向磁盘中写入东西,那么系统就会把这些文件归为D状态,D状态不可被外界打断,只能由进程自己苏醒,这一状态可以保证东西都完美写入到磁盘中。(磁盘也很快,一般不会吹西安这种状态,如果频繁出现这种状态,可能是你磁盘老化了)
  • 情景:假设某个进程需要读取数据,而这个数据在磁盘上,那么首先是需要磁盘驱动程序将磁盘上的数据读取到内核的文件缓冲区,这个由磁盘驱动程序将磁盘数据读取到内核文件缓冲区的过程就是磁盘IO,而这个过程中,当前这个进程是不参与的,所以他会等待磁盘IO完成,这个他这个等待的过程就是D状态。此时想要杀死D进程,就要先处理磁盘IO。
    • 需要补充的是:磁盘IO不需要CPU参与,牢记冯诺依曼结构,CPU只会和内存打交道。

T

  • 暂停(因为ctrl z暂停)。
  • 可以通过jobs指令查看当前处于休眠的任务,同时会回显任务号。然后通过kill -9 %任务号指令就可以删除这个休眠的任务。也可以通过kill -SIGCONT %任务号让休眠进程继续进行。或者使用fg %任务号放在前台运行,使用bg %任务号放在后台运行。

t

  • 暂停(因为debug暂停)

Z(Zombel)

  • 僵尸状态:当子进程完成(或者被kill),子进程的PCB(tash_struct)此时没有父进程回收,就把此时的子进程状态称为僵尸状态。
  • 对于僵尸状态,如果你kill父进程,那么子进程终止的时候就会导致没有父进程回收task_struct,为了解决这一问题,操作系统会自动把你这个子进程的pid设为1,这个1其实就是bash的进程号。当你子进程运行完毕,task_struct就会交给bash回收,防止内存泄漏。

僵尸状态验证代码

c 复制代码
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>

int gval = 100;

int main()
{
    printf("父进程开始运行,pid: %d\n", getpid());
    pid_t id = fork();
    if (id < 0)
    {
        perror("fork");
        return 1;
    }
    else if (id == 0)
    {
        printf("我是一个子进程 !, 我的pid: %d, 我的父进程id: %d, gval: %d\n", getpid(), getppid(), gval);
        sleep(5);
        // child
        while (1)
        {
            sleep(1);
            printf("子进程修改变量: %d->%d", gval, gval + 10);
            gval += 10; // 修改
            printf("我是一个子进程 !, 我的pid: %d, 我的父进程id: %d\n", getpid(), getppid());
        }
    }
    else
    {
        // father
        while (1)
        {
            sleep(1);
            printf("我是一个父进程 !, 我的pid: %d, 我的父进程id: %d, gval: %d\n", getpid(), getppid(), gval);
        }
    }
    printf("进程开始运行,pid: %d\n", getpid());

    //  chdir("/home/whb");
    //  fopen("hello.txt", "a");
    //  while(1)
    //  {
    //      sleep(1);
    //      printf("我是一个进程 !, 我的pid: %d, 我的父进程id: %d\n", getpid(), getppid());
    //  }
}

僵尸状态效果

X

  • 指结束的进程状态,通常情况下看不到,因为一进入X,PCB被回收,就瞬间查看不了此进程的状态。

++想要验证R状态,只需要运行死循环while(1){}即可,想要验证S,只需要scanf()阻塞住即可。++

++一定要注意:验证R状态,万万不要涉及文件IO,涉及printf scanf之类的都不可以,IO会导致进程大多数时间处于S状态,你可能会看到S状态。++

挂起状态

  • 挂起状态指:现在内存紧张,需要把一部分不重要的进程的代码和数据从内存移到磁盘上,那么这些本在内存,但是移到磁盘上的进程就进入了挂起状态。磁盘上存放这些进程代码和数据的地方就是swap分区。当内存够用了,再把swap分区内容移到内存上。在这个过程中,页表也会把对应代码和数据的物理地址与磁盘上的逻辑地址像变换。(页表就是完成虚拟地址和物理地址的映射关系,详细的页表后续会单出一个文章,详细的物理地址和虚拟地址变换也会单出一个文章)

  • 这个状态的相关操作由操作系统完成,用户通过ps指令看不到这个状态

相关推荐
虚伪的空想家3 小时前
K8S部署的ELK分片问题解决,报错:unexpected error while indexing monitoring document
运维·elk·云原生·容器·kubernetes·报错·eck
YXXY3134 小时前
算法练习(C++)---双指针
c++
yanqiaofanhua5 小时前
C语言自学--数据在内存中的存储
c语言·开发语言
玖笙&6 小时前
✨WPF编程基础【1.3】:XAML 名称空间
c++·wpf·visual studio
玖笙&6 小时前
✨WPF编程基础【1.4】:类型转换器(含示例及源码)
c++·wpf·visual studio
计算机软件程序设计8 小时前
基于Python的二手车价格数据分析与预测系统的设计与实现
开发语言·python·数据分析·预测系统
大聪明-PLUS8 小时前
如何从头开始开发 Linux 驱动程序
linux·嵌入式·arm·smarc
心灵宝贝9 小时前
CentOS 7 安装 net-tools.rpm 包步骤详解(附 rpm 命令和 yum 方法)附安装包
linux·运维·centos
1024find9 小时前
Linux基线配置
linux·运维·服务器