Linux进程概念

发布于:2024-04-19 ⋅ 阅读:(29) ⋅ 点赞:(0)


前言

在本篇文章中我们将会学到有关进程的一系列内容,包括进程的PCB,系统调用函数getpid,folk以及进程的状态(运行,阻塞,挂起),僵尸进程,孤儿进程,优先级,进程调度算法等具体内容!!!

一、进程PCB

我们在运行多个.exe文件时,需要首先加载到内存。操作系统要不要管理多个加载到内存的程序呢??肯定是要的。那么操作系统如果进行管理呢??
先描述,在组织
我们描述的这个结构体就被叫做进程控制块(PCB),进程属性的集合。在Linux中PCB是tast_struct。它会被装载到内存中并且包含进程的信息,我们创建这个PCB对象就是为了方便操纵系统进行管理。
我们开机时启动的第一个程序就是我们的操作系统。

进程=内核PCB+可执行程序。

我们未来所有对进程的控制和操作,都之和进程的PCB有关,与进程的可执行程序没有关系。
操作系统将每一个进程都进行描述,形成一个个PCB,并将这些PCB以双链表的形式组织起来。
在这里插入图片描述
我们只需要一个头指针就可以访问所有的PCB了。
我们对进程的管理就变成了对PCB对象的管理,对链表的增删查改。

我们具体看一下PCB内部都有什么

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

这些具体什么我们后面都会进行阐述。
我们这里看一下程序计数器:简单了说就是看这个程序变成进程之后运行到了第几行。
每一行代码都有对应的地址。
CPU中与一个epi或者pc指针这样的概念用来记录。记录当前正在执行指令的下一条指令的地址。
CPU虽然很快,但是很笨,它只能完成这样的任务:取指令–>分析指令–>执行指令。
判断,循环,函数跳转本质:修改pc指针。
pc指向哪一个进程的代码,就表示哪一个进程被调度执行。

PCB中的数据其实是操作系统内部的数据,我们用户不能直接访问操作系统,需要通过系统调用接口来调用。

其中记账信息中历史数据决策后续动作

二、gitpid,gitppid与fork函数

1.getpid/gitppid

gitpid是系统调用中用来获取进程标识符的,gitppid是系统调用中用来获取这个进程的父进程的进程标识符的。

看一下gitpid文档介绍
#include< sys/types.h>和#include< unistd.h>

我们写一段简单的代码

 #include <stdio.h>
 #include <sys/types.h>
 #include <unistd.h>
 int main()
 {
    printf("pid: %d\n", getpid());
    printf("ppid: %d\n", getppid());
    return 0;
  }

我们多运行几次看一下

在这里插入图片描述

我们可以发现一下结论

在Linux中,普通进程,都有它的父进程。
每启动进车给的pid几乎都会变化,因为我的进程是一个新的进程。

我们发现父进程都是一样的,我们查看一下,
在这里插入图片描述
父进程就是bash.

我们如何查看进程的信息呢??

🌟🌟使用ps命令,可以显示所以进程信息
ps搭配grep工具,可以显示某个进程的信息。

ps ajx |head -1 && ps ajx | grep myprocess

我们先让这个进程跑起来,我们利用死循环来查看,如果的是普通代码,CPU跑的会非常快,我们根本捕捉不到相应的信息

 int main() 
 {         
     while(1)
     {
        printf("I am a process,pid: %d\n", getpid());                                                
     }                                        
     return 0;                                
  }

在这里插入图片描述
我们可以发现按grep确实帮助我们把对应的进程过滤出来了,
我们注意一下最后一行是什么,grep本质也是指令,它在执行时需要用到myprocess,所以他把自己也过滤出来了。
几乎所有独立的指令,运行起来也要变成进程。进程也是有生命的

我们如果不想包括这个grep,我们可以在后面+grep -v grep,反向匹配
在这里插入图片描述

🌟🌟进程相关的内存级数据以及文件会以文件系统形式显示在某个目录下

我们可以通过相应的指令查看

ls /proc/n n是标识符

在这里插入图片描述
这里面包含了一堆进程信息,其中很明显的是cwd和exe
exe我们很清楚,就是可知行程序,我们看一下cwd
根据指向的内容,看着好像是当前路径,是的!!!
cwd:当前进程的工作目录

我们在c语言学过fopen这个函数,如果这个文件不存在,就会在当前路径下,创建一个文件。那麽这个当前路径其实就是当前进程的工作目录。

我们看一下下面这个现象,我们已经把文件从磁盘中删除了,为什么程序还在跑呢??
在这里插入图片描述
我么可以看到exe变红了,表明我们确实已经删除这个程序了。
可执行程序首先要被加载到内存,本质就是把磁盘中的代码拷贝到内存中,磁盘中的代码已经删除了,那和我内存有什么关系??
所以这个程序一直再跑。

我们也可以用chdir这个函数修改当前工作目录

我们这个程序运行起来,变成进程之后,会记录可执行文件是谁,工作目录在哪里。

2.folk()

fork:通过系统调用创建进程

我们首先看一下文档
在这里插入图片描述
2号手册,系统调用函数,需要头文件#include< unistd.h> 返回值是一个pid_t类型,本质就是int

int main()
{
    printf("beforefork:pid:%d,ppid:%d\n",getpid(),getppid());                                     
    fork();
    printf("after fork:pid:%d,ppid:%d\n",getpid(),getppid());
}

我们看一下运行结果
在这里插入图片描述

代码怎末运行了三次??
fork之后,代码运行了两次??我们首先观察一下这几个pid和ppid
第一行,ppid我们很清楚了就是bash,pid就是发当前进程的标识符
第二行,pid和ppid和第一行完全一样
第三行,拥有自己的pid,但是ppid却是第二行的pid

一开始是父进程bash23681,创建子进程25877,fork之后,分为了父进程25877和子进程25878.
也就是说23681和25878是爷孙关系。
同时:fork之后代码共享,要不然怎末能运行两次。

我们看一下fork的返回值
在这里插入图片描述
创建成功之后,fork之后父进程返回子进程的pid,子进程返回0。
创建失败,父进程返回-1,不创建子进程。

int main()
{
    printf("before  fork:pid:%d,ppid:%d\n",getpid(),getppid());
    pid_t id=fork();
    printf("after fork:pid:%d,ppid:%d,return id:%d\n",getpid(),getppid(),id);                       
 }

在这里插入图片描述

我们为什么要折磨做呢??当然是想让子进程和父进程做不一样的事情了
我们可以通过返回值来控制做不同的事情!!使用if分流

int main()
{
    pid_t id = fork();
    if(id < 0) return 1;
    else if(id == 0)
    {
        // 子进程
        while(1){
            printf("after fork, 我是子进程: I am a prcess, pid: %d, ppid: %d, return id: %d\n", getpid(), getppid(), id);
            sleep(1);
        }
    }
    else{
        // 父进程
        while(1){
            printf("after fork, 我是父进程: I am a prcess, pid: %d, ppid: %d, return id: %d\n", getpid(), getppid(), id);
            sleep(1);
        }
    }
    return 0;
}

我们看一下父进程和子进程的底层

在这里插入图片描述

我们在使用fork时,系统会多一个进程。
子进程中也要指向父进程的代码和数据,父子代码共享,只不过通过分流做不同的事情。
子进程的PCB也要从父进程中来,大部分都是一样的,但是pid可能就不同,在把自己对应的那部分填充起来。
子进程被创建,是以父进程为模板的
当子进程用到其中的代码和数据,只有子进程用,就会额外开辟一块空间给子进程或者数据修改,发生写时拷贝。

实际上,使用folk函数创建子进程,在folk函数被调用之前的代码被父进程执行,folk函数之后的代码,父子都可以执行。

我们来看一下下面这个几个问题??

🔱🔱为什么父进程返回子进程的pid,给子进程返回0呢??
父子直接是一对多的关系,一个父亲可能有多个孩子,父亲需要有对应的标识符对子进程进行管理和控制。而对于子进程,只需要关系自己是否被创建就可以。
🔱🔱fork函数为什么返回两次??
在这里插入图片描述
父进程在调用fork时,需进行一系列操作创建子进程,fork在执行return之前,子进程已经被创建完毕了,父进程需要返回,同时子进程也需要返回。
🔱🔱id怎么可能同一个变量,即大于零,又等于0??
一个进程崩溃了,会不会影响其他进程呢??当然不会。进程之间要具有独立性,互相不影响。
在Linux中,可以用同一个变量名表示不同的内存。
就比如c语言在编译时,就不会看变量名了,而是转化为对应的地址。

三、进程状态

一个进程从创建而产生到消亡的整个生命期间,有时占有处理器执行,有时虽然可以运行但是分不到资源,有时虽有空闲资源但因等待某个时间的发生而无法执行,这一切都说明进程是活动的且变化的,于是就有了进程状态这一概念。

所谓的状态;本质就是一个在task_struct中的一个整形变量。
状态决定了你的后续动作,Linux中可能会存在多个进程都要根据它的状态执行后序动作,进程在排队。

1.运行

并不意味着进程一定在运行中,运行是进程要抹在运行中,要抹在运行队列中。

所有处于运行状态,即可被调度的进程,都被放到运行队列中,当操作系统需要切换进程运行时,就直接在运行队列中选取进程运行。

在这里插入图片描述

2.阻塞

当我们的程序在运行队列中运行的时候,可能需要等待某种软硬件资源,如果这个资源没有就绪,这时我们就称为阻塞状态。
在阻塞状态,我们的task_struct会把自己设置为阻塞状态,同时把自己的PCB连入等在资源提供方的等待队列。
那么这个硬件是否就绪谁最清楚??当然是操作系统,因为操作系统是管理者。
当这个资源就绪,操作系统就会重新把PCB从等待队列转移到运行队列,同时需改状态值。

状态的变迁,引起的是PCB会被操作系统变迁到不同的队列中。

3.挂起

前提:计算机资源已经很紧张了。

我们通过一张图来解释一下
在这里插入图片描述
内存中有很多进程,其中每个进程都有自己对应的代码和数据。
现在内存已经比较紧张了,马上就不够用了,如果继续申请,就可能会面临奔溃的情况。所以操作系统就会想办法腾出一部分内存。但是有些进程虽然占用着内存,但是短期之内不会就绪。
有些内存占着不用也是一种浪费。
所以操作系统会把这部分代码和数据放入到磁盘中的一个swap分区,那麽内存就只有这个进程,没有对对应的代码和数据,这种状态我们称为挂起

从内存将数据和代码拷贝到外设称为唤出
将代码和数据从磁盘拷贝到内存称为唤入

其中PCB一直在内存中,不会参与唤出唤入过程。
当内存不那么紧张了,再把代码和数据唤入到内存中,继续执行。

我们一个程序变成进程之前,是先创建PCB的,而不是先加载到内存。
这个swap分区大小一般都是等于内存的大小护着等于一半内存,如果swap分区太大,内存和磁盘之间的IO操作就会变多,从而产生依赖。

4.Linux具体进程状态

我们查看一下Linux内核中进程具体有哪些状态

/*
* 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状态

int main()
{
    while (1)
    {
        printf("hello world\n");
    }
    return 0;
}

在这里插入图片描述
printf函数需要访问外设,外设是很慢的,CPU资源运行很快,运行完之后再次需要等待显示器资源,但是等待显示器资源占用了几乎所有的时间,
只有一行代码,大部分时间都在等在显示器资源,显示器是外设资源,外设比CPU慢得多,不一定处于就绪状态。所以大部分时间都处于休眠状态。

我们去掉printf函数,仅仅cpu实现死循环
在这里插入图片描述
grep–>R grep也是进程。几乎所有独立的指令,就是程序,运行起来也要变成进程。被运行起来才可以进行过滤操作。

我们注意一下:
R+:这其实是前台进程,我们可以通过键盘杀掉
在这里插入图片描述

R:后台进程,键盘不能杀掉,只能通过kill杀掉, ./myprocess &
在这里插入图片描述

🌟🌟S状态和D状态 休眠状态

我们看一下S和D都是休眠,我们看一下具体的区别
S:意味着进程等待事件完成,称为可中断睡眠,也称为浅度睡眠
D:这个状态的进程通常会等待IO结束,称为不可中断睡眠,也称为深度睡眠。

ctrl+c可以中止S+(可中断睡眠)浅度睡眠
深度睡眠表明该进程不会被杀掉,操作系统也不行,只要该进程自动唤醒才可以恢复,处于这个状态的进程通常会等待IO的结束。

我们通过一个小故事深入了解一下深度睡眠状态
在这里插入图片描述

现在操作系统要求内存中进程1向磁盘中写100B的数据,进程1就发命令了给磁盘,并告诉磁盘,你写完之后告诉我一下写的怎摸样,写完了吗??
磁盘在自己着找了一块空间就开始写了,但是外设一般都比较慢,所以这个进程1就在这无聊的等着。
操作系统中特别忙,一会忙着一会忙那,一看这个进程1在这里玩,就等着是否写完了。操作系统一看你这么闲,就把你干掉了,分配给其他地方,这时磁盘中还没有写完呢??
磁盘最后发现自己空间不够了,还剩下一点没有写完,就要告诉进程1,但是进程1已经被干掉了,找不到了。
最后用户过来查看一下,用户以为都写到了磁盘中,其实并没有。比如这个数据是淘宝最近1个小时交易额,最后对不上账,这就麻烦了。
这时用户就问操作系统,进程,磁盘,看看是谁出的问题??
用户就问操作系统你把哪个进程干掉了,导致的这个问题,操作系统说:为我干的任务不都是你用户交给我的吗??如果我不把这个进程干掉,有可能损失的就更多了,用户一听没问题啊。
又问进程,进程说:我是受害者啊,在这里好好的就被干掉了,我的任务就是等着磁盘告诉我写的怎末样了。接着问磁盘:我就是个跑腿的,别人让我干什么我就干什么!!
用户一看都没有问题啊,难道是我的问题??用户就把操作系统改了,把这个进程1放一块“免死金牌”,进程是不允许杀掉你的。这个免死金牌就是D状态

操作系统在逼急了的情况下是可以杀掉进程的。就比如我们多个人在抢票,人数众多的情况下就会崩溃掉,大学生选课时可会面临这种情况。
🌟🌟T暂停状态

我们看一下T,这其实是暂停状态,这有两个T,但本质都是一样的
我们第一个可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行。

kill -l 查看信号,19号暂停,当操作系统感觉遇到危险操作时,但又不想杀掉你这进程,就会让你暂停。

在这里插入图片描述
我们也可以通过gdb调式看到暂停状态

我们仔细看一下上面这四种状态,就是阻塞的具体情况

🌟🌟Z状态和X状态 死亡状态

Z和X是死亡状态,但是两者有着很大的区别

其中X就是正常退出,PCB和数据都已经删除。
Z我们一般称为僵尸状态

四、僵尸进程和孤儿进程

1.僵尸进程

当一个进程要退出的时候,在系统层面,该进程曾经申请的资源不是立即释放,而是要暂时存储一段时间,以供操作系统或者父进程进行读取。
一般用于父子进程之间,子进程先退出了,但是PCB还没有退出,父进程还没有读取子进程的PCB,父进程还在运行,子进程进入僵尸状态。
需要等待父进程进行读取,父进程读取完毕之后,子进程PCB再退出。

#include <stdio.h>
#include <stdlib.h>
int main()
{
    pid_t id = fork();
    if (id < 0) {
        perror("fork");
        return 1;
    }
    else if (id > 0) { //parent
        printf("parent[%d] is sleeping...\n", getpid());
        sleep(30);
    }
    else {
        printf("child[%d] is begin Z...\n", getpid());
        sleep(5);
        exit(EXIT_SUCCESS);
    }
    return 0;
}

exit()可以让子进程直接退出。
在这里插入图片描述
为什莫会有Z状态:我们希望子继承完成任务后,把完成的情况告诉给父进程,以便进行相应的后续操作。

如果父亲一直不读取,子进程一直处于僵尸状态,僵尸进程PCB就一直在内存中等待,一直存在。
如果一个父进程创建了很多子进程,但都不进行回收,那麽久造成资源的浪费。父进程如果不进行读取就会发生内存泄露。
bash会自动读取父进程,所以我们平常看不到

2.孤儿进程

孤儿进程:父进程先退,子进程就称为孤儿进程


int main()
{
    pid_t id = fork();
    if (id < 0) {
        perror("fork");
        return 1;
    }
    else if (id == 0) {//child
        printf("I am child, pid : %d\n", getpid());
        sleep(10);
    }
    else {//parent
        printf("I am parent, pid: %d\n", getpid());
        sleep(3);
        exit(0);
    }
    return 0;
}

在这里插入图片描述

父进程先退,子进程被系统一号进程(操作系统)领养 ,就由一号进程进行回收
由前台进程S+变成后台进程S

Linux下进程的具体是这样的
在这里插入图片描述

五、进程优先级

🌟进程排队:一定是在等在某种资源

只要是排队,一定是进程的PCB在排队,比如找工作投简历,并不是有你一直在那里排着队,而是你的信息(PCB)在排队。

🌟进程不是一直在运行的,就比如下面这段代码
在这里插入图片描述
运行到scanf就会停下来,停下来干神魔呢??根据我们所学习到的内容,我们需要从键盘输入才可以,那麽久说明这个这个进程在等待键盘资源。
🌟进程放到了CPU上,也不是一直在运行的
比如:写了一个死循环。CPU如果一直在跑这个死循环,其他的进程就啥也不干了。
所以我们会给这个进程分配一个时间,你这个进程就跑1ms,然后停下来。这个就称为时间片的概念。

进程优先级:进程要访问某种资源,进程通过一定的方法进行排队,确认cpu资源分配到先后顺序。优先级越高,就先执行。
还可以把进程运行到指定的CPU上,这样一来,把不重要的进程安排到某个CPU,可以大大改善系统整体性能

为啥要排队??所需求的资源总是占少数的!!!
我们注意区分一下优先级和权限,权限是能不能干,优先级是谁先干。

ps-l 显示进程的优先级
在这里插入图片描述
我们很容易注意到其中的几个重要信息,有下:
UID : 代表执行者的身份
PRI :代表这个进程可被执行的优先级,其值越小越早被执行
NI :代表这个进程的nice值

优先级本质就是数字,数字越小,优先级越高。PRI默认都是80.
进程的优先级是可以进行修改的,但并不是直接修改PRI,而是通过修改NI值间接修改PRI。
这个NI也称为修正数据。Linux优先级范围是【60,90】,这个NI也是有范围的,【-20,19】。

PRI(new)=PRI(old)+NI;
调整优先级,在Linux下,就是调整NI值。

具体我们如何通过代码修改呢??

top–>按r–>输入要修改的PID–>NI值

在这里插入图片描述
每次修改都是从80开始的。

Linux为什莫调整优先级是要受限制的??
如果不加限制,将自己进程的优先级调整到非常高,别人的优先级调整到非常低。优先级较高的进程,优先得到资源,后序还会有很多进程产生,常规进程就很难享受CPU资源,就会产生进程饥饿问题
任何的分时操作系统,在调度上,都会进行较为公平的调度,如果不加限制,就会变得很不公平。

🌟竞争性: 系统进程数目众多,而CPU资源只有少量,甚至1个,所以进程之间是具有竞争属性的。为了高效完成任务,更合理竞争相关资源,便具有了优先级
🌟独立性: 多进程运行,需要独享各种资源,多进程运行期间互不干扰
🌟并行: 多个进程在多个CPU下分别,同时进行运行,这称之为并行
🌟并发: 多个进程在一个CPU下采用进程切换的方式,在一段时间之内,让多个进程都得以推进,称之为并发。
并发就像以前的动画片,都是画出来的,一卡一卡的播放,如果足够快,我们就看不出来卡顿,就认为是同时推进的。

进程切换与调度

所有的保存都是为了最终的恢复,所有的恢复都是为了继续上次的运行位置继续运行。
CPU中存在大量的寄存器,进程在运行过程中,要产生大量的临时数据,这些临时数据放在CPU中的寄存器中,这些放在寄存器上的数据就叫做硬件上下文。
当时间片到了,结束运行,就会把这个硬件上下文数据放到PCB中的一个地方,保护硬件上下文。
当再一次调度时,就会重新把这些数据拷贝到CPU寄存器中,将曾经的硬件上下文进行恢复,继续运行程序。

CPU中的寄存器只有一套,但是寄存器内部保存的数据可以有多套。
虽然寄存器数据放到了一个共享的CPU设备里面,但是所有的数据,其实都是被进程私有的。

六、 Linux内核进程调度队列

Linux内核进程调度队列考虑优先级,饥饿,效率

在这里插入图片描述

一个CPU只拥有一个运行队列

普通优先级:100- -139
实时优先级:0 - -99
我们进程的都是普通的优先级,前面提到的nice值取值范围是【-20,19】,一共40个级别,依次对应queue当中普通优先级下标【100,139】。
实时优先级对应实时进程,实时进程是先将一个进程执行完毕再执行下一个进程,现在基本不存在这种机器了,所有【0,99】我们不关心。

🌟🌟活动队列
时间片还没有结束的进程都按照优先级放到这个队列中
在这里插入图片描述

nr_active:总共还有多少个运行状态的进程。

queue[140]:一个元素就是一个进程队列,相同优先级的进程按照FIFO规则进行排队调度,数组下标就是优先级。
我们正常运行的时候,从下标0开始,一直到139,一个个运行,如果非空就拿到队列中的进程,开始运行,运行完毕后,继续下一个。遍历一遍效率有点低。

我们采用位图的方法可以极大的提高效率
bitmap[5]:一共140个优先级,一共140个进程队列。用这五个整数的比特位表示是否队列是否为空。比特位的位置表示在哪个队列,这样我们就可以极大的提高效率。
🌟🌟过期队列
如果没有这个队列,新的进程或者时间片到期的进程就会重新放到对应的位置,继续运行,优先级低的进曾就很难运行到,就很容易发生进程饥饿问题。
我们采用过期队列就可以很好的解决这种问题。
过期队列和运行队列拥有相同的结构。
如果程序在运行队列时间片耗尽,就会被挪动到过期队列,或者是有新加入的进程。
当活动队列上的进程都被处理完毕之后,对过期队列的进程进行时间片的重新计算。
🌟🌟active和expired指针
active指针指向活动队列,expired指针指向过期队列。
CPU跑起来,活动队列的进程越来越少,过期队列的进程越来越多。当活动进程的进程都完毕了,而过期队列积攒了一大片的进程,等着被调度。
我们只要交换这两个指针的内容,就相当于具有了一批新的活动进程。

在系统当中查找一个最合适调度的进程的时间复杂度是一个常数,不随着进程增多而导致时间成本增
加,我们称之为进程调度O(1)算法!

总结

以上就是今天要讲的内容,本文仅仅详细介绍了Linux关于进程,包括进程的PCB,系统调用函数getpid,folk以及进程的状态(运行,阻塞,挂起),僵尸进程,孤儿进程,优先级,进程调度算法等内容。希望对大家的学习有所帮助,仅供参考 如有错误请大佬指点我会尽快去改正 欢迎大家来评论~~ 😘 😘 😘