• 【Linux】进程基础



    在这里插入图片描述

    1.冯诺依曼体系

    我们常见的计算机,如笔记本。我们不常见的计算机,如服务器,大部分都遵守冯诺依曼体系 。

    image-20220808212343448

    组成部分

    • 输入设备:键盘、话筒、摄像头、磁盘、网卡…
    • 输出设备:显示器,音响,磁盘,网卡,显卡…
    • 中央处理器(CPU):算数计算+逻辑运算
    • 存储器:内存

    为什么要有内存?

    • 1.从技术的角度:

      • CPU的运算速度 > 寄存器的速度 > L1~L3Cache【三级缓存】> 内存 > 外设【磁盘】> 光盘磁带
      • 外设不会之间和CPU交互,而是先和内存交互。然后CPU再和内存进行交互
      • 从整个内存来看,整个体系结构就是一个大的缓存,解决外设和CPU速度不匹配的问题
    • 2.从成本角度

      • 寄存器 > 缓存 > 内存 > 外设
    1.2操作系统

    操作系统包括:

    • 内核:内存管理,进程管理,文件管理,驱动管理
    • 其他程序

    操作系统可以看成是管理软件的软件。

    image-20220808213247931

    • 在开发角度,操作系统对外会表现为一个整体,但是会暴露自己的部分接口,供上层开发使用,这部分
      由操作系统提供的接口,叫做系统调用。
    • 系统调用在使用上,功能比较基础,对用户的要求相对也比较高,所以,有心的开发者可以对部分系统
      调用进行适度封装,从而形成库,有了库,就很有利于更上层用户或者开发者进行二次开发

    2.进程

    2.1进程的概念
    • 进程是一个运行起来的程序(程序是一个可执行的文件)
    • 内核:担当分配系统资源(CPU时间,内存)的实体。
    • 进程=可执行文件+描述进程的数据结构(task_struct)

    为什么管理描述进程要有-PCB(process ctrl block)?
    进程信息被放在一个叫做进程控制块的数据结构中,可以理解为进程属性的集合。
    课本上称之为PCB(process control block),Linux操作系统下的PCB是task_struct 。(因为Linux内核是用C语言写的,所以Linux管理内核就使用一个结构体存储一个进程的信息,以便管理)

    • 在Linux中,所有运行在系统里的进程都以task_struct链表的形式存在内核里

    查看进程

    查看进程
    ls /proc
    也可以使用psc
    ps ajx
    
    • 1
    • 2
    • 3
    • 4

    image-20220808213614762

    2.2 task_struct

    task_ struct内容分类

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

    为什么父进程的父进程不变?

    所有进程的最终父进程是bash进程,几乎我们在命令行上所执行的所有的指令(cmd),都是bash进程的子进程。

    为什么fork(),子进程会返回0,而父进程会返回子进程的pid?

    子进程的task_struct对象,内部的数据基本上都是从父进程拷贝而来的。fork()之后,父子进程的代码共享,但是一般都通过不同的返回值,让不同的进程执行不同的代码。【而数据是各自独立】

    如何理解进程被运行?

    理解调度器和调度队列(runqueue)

    • 创建进程
    • 放入运行队列
    2.3进程的状态

    操作系统层面的进程的状态status

    image-20220705015259759

    • 进程运行:并不意味着进程一定在运行中,它表明进程要么是在运行中要么在调度【运行】队列里。
    • 进程终止:表示这个进制永远不再使用,随时等待被释放
    • 进程阻塞:
      • 1.一个进程,在使用资源的时候,不仅仅要申请CPU资源
      • 2.进程可能要申请更多的其他资源:磁盘、网卡、显卡、显示器资源…
      • 3.在申请CPU资源的时候,可能暂时无法得到满足,需要排队-----运行队列
      • 4.如果要申请其他的慢资源(比如外设)------也需要进行排队【申请各个资源的等待队列】(task_struct排队)。
      • 5.当进程申请其他某些资源,而资源暂时没有准备好,或者在为其他进程提供服务。此时:1.当前进程要从runqueue中移除。2.将进程的task_struct放入对应设备的描述结构体中的等待队列。
      • 6.当进程在等待某种资源的时候,资源没有就绪,进程需要在该资源的等待队列中进行排队,进程的代码不会被执行,也就是进程阻塞。
    • 进程挂起:
      • 当进程过多时,内存可能不足,这是OS就会对部分进程进行辗转腾挪
      • 短期时间不会被调度的进程(可能是因为某资源的等待队列过长,短期内无法申请到某资源),它的代码和数据依旧在内存中占用资源,就是白白的浪费空间。OS就会把进程的代码和数据从内存置换到磁盘中【磁盘中有一个专门存放置换进程代码和数据的分区,叫做swap分区】。
      • 因为内存不足和展示不被调度,而导致进程的数据置换到磁盘的swap分区,就叫做进程挂起。

    系统为什么要维护一个终止态?

    • 可能系统需要进程终止的信息
    • 释放进程需要时间,可能此时操作系统比较繁忙。
    • 该状态下,进程永远也不运行,随时等待被释放。

    Linux的进程状态state

    task_struct中使用task_state_array[]数组存放状态变量
    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 */[重点]
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    image-20220809175358359

    • R运行状态(running): 并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列里。
    • S睡眠状态(sleeping)-----对应阻塞状态: 意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡眠(interruptible sleep)),该进程可以被OS杀死
    • D磁盘休眠状态(Disk sleep)-----对应阻塞状态:有时候也叫不可中断睡眠状态(uninterruptible sleep),**此状态下OS无法杀死该进程,**在这个状态的进程通常会等待IO的结束。
      • 一般而言,Linux中,如果我们等待的是磁盘资源,进程阻塞的状态就是D。
    • T停止状态(stopped): 可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行。
    • t(tracing stop):也是停止状态,是调试状态下的暂停状态。
    • X死亡状态(dead):这个状态只是一个返回状态,进程可以被释放,你不会在任务列表里看到这个状态。
    • Z僵尸状态(zombie):在Linux中,一个进程退出的时候,一般不会直接进入X状态(死亡状态,资源立马回收),而是进入Z状态。
      • 为什么?
      • 一个进程被创建出来,一定是因为要有认为让这个进程执行,当该进程退出的时候,需要告知父进程和OS执行任务的情况。
      • 进程进入Z状态,就是为了维护退出信息,可以让父进程或者OS读取
      • 僵尸进程就是Z状态。
      • 维护退出状态本身就是要用数据维护,也属于进程基本信息,所以保存在task_struct(PCB)中,换句话说,Z状态一直不退出,PCB一直都要维护。
      • 那一个父进程创建了很多子进程,就是不回收,就会造成内存资源的浪费。对象本身就要占用内存,想想C中定义一个结构体变量(对象),是要在内存的某个位置进行开辟空间 。

    孤儿进程:父进程先死,子进程还没有被杀掉。子进程会被1号进程领养。而1号进程就是【操作系统】。

    进程的大部分时间都在等待申请资源

    int main()
    {
        printf("begin.....\n");
        while(1)
        {
            
        }
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    在这里插入图片描述

    int main()
    {
        printf("begin.....\n");
        while(1)
        {
            printf("hello world\n");
        }
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    ps ajx | head -1 && ps ajx | grep hello
    
    • 1

    在这里插入图片描述

    表明进程大部分的时间都在申请资源。

    2.4进程优先级
    优先级VS权限

    优先级是进程获取资源的先后顺序。

    • cpu资源分配的先后顺序,就是指进程的优先权(priority)。
    • 优先权高的进程有优先执行权利。配置进程优先权对多任务环境的linux很有用,可以改善系统性能。
    • 还可以把进程运行到指定的CPU上,这样一来,把不重要的进程安排到某个CPU,可以大大改善系统整体性能
    为何会存在优先级

    资源不够。系统里面永远都是,进程占大多数,而资源是少数。

    Linux下的优先级相关概念
    ps -la
    
    • 1

    image-20220810174242329

    相关概念

    • UID : 代表执行者的身份
    • PID : 代表这个进程的代号
    • PPID :代表这个进程是由哪个进程发展衍生而来的,亦即父进程的代号
    • PRI :代表这个进程可被执行的优先级,其值越小越早被执行
    • NI :代表这个进程的nice值

    PRI和NI

    • PRI,即进程的优先级,或者通俗点说就是程序被CPU执行的先后顺序,此值越小进程的优先级别越高
    • NI,就是我们所要说的nice值了,其表示进程可被执行的优先级的修正数值
    • PRI值越小越快被执行,那么加入nice值后,将会使得PRI变为:PRI(new)=PRI(old)+nice
    • 这样,当nice值为负值的时候,那么该程序将会优先级值将变小,即其优先级会变高,则其越快被执行
    • 所以,调整进程优先级,在Linux下,就是调整进程nice值;nice其取值范围是-20至19,一共40个级别。Linux给用户用的进程优先级范围是80-100。
    • 需要强调一点的是,进程的nice值不是进程的优先级,他们不是一个概念,但是进程nice值会影响到进程的优先级变化。
    • 可以理解nice值是进程优先级的修正修正数据

    **用top命令更改已存在进程的nice **

    • top
    • 进入top后按“r”–>输入进程PID–>输入nice值
    • 或者PID to renice
    • Linux不允许用户无节制的设置优先级
    2.5其他重要概念

    竞争性: 系统进程数目众多,而CPU资源只有少量,甚至1个,所以进程之间是具有竞争属性的。为了高效完成任务,更合理竞争相关资源,便具有了优先级

    独立性: 多进程运行,需要独享各种资源,多进程运行期间互不干扰

    并行: 多个进程在多个CPU下分别,同时进行运行,这称之为并行

    并发: 多个进程在一个CPU下采用进程切换的方式,在一段时间之内,让多个进程都得以推进,称之为并发 。

    单道和多道程序设计

    单道程序设计:一次只有一个处于程序在运行中;其他程序处于等待状态。(DOS)

    多道程序设计:设计出时间片,在一个时间片中执行一个程序,在下一个时间片立马然后切换为下一个程序,从而让出cpu的资源给其他程序。

    由于一个时间片的时间很短,属于是毫秒级别的。所以在人的感知上,几个程序是并发进行的;但是在微观上,在一个时间片上,只有一个程序在运行。微观上串行,宏观上并行

    image-20220705013224488

    进程的切换是依靠调度器。

    • 在一段时间内,多个进程都会通过切换交叉的方式,让多个进程在这段时间内得到推进,这种现象叫做并发。

    操作系统,就是简单的根据队列进行先后调度吗?有没有可能突然来了一个优先级更高的进程?

    • 当代计算机,都符号抢占式内核。
    • 正在运行的低优先级进程,如果来了优先级更高的进程,调度器会直接把进程从CPU上剥离(无论低优先级的时间片是否跑完),给优先级高的进程,这就是抢占式内核。
    2.5Linux维护进程的方式(O(1)调度)

    依靠hash表和位图

    task_struct* queue[140];
    根据不同的优先级,将特点的进程放入相应的队列中。
    比如优先级为2的进程,放入第二个task_struct queue中。
    
    • 1
    • 2
    • 3

    在这里插入图片描述

    维护方式:

    ​ 当新增一个进程时(进程的优先级为n),将改进程放入queue[n]队列的末尾。这种方式也维护了进程的优先级,即高优先级的先运行。

    进程切换

    CPU内的寄存器,可以临时的存储数据

    int hello()
    {
        int a=10+20;
        return a;
    }
    int b=hello();
    /*
    	当返回值较小时
    	a是临时变量,在返回时被销毁,系统会把a对应的值放入寄存器eax中;
    	然后寄存器eax再将值mov给b对应的内存
    */
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    进程进行切换的时候,进程需要被CPU剥离。
    在这里插入图片描述

    • 上下文数据保存在哪?
    • Linux的PCB,task_struct中有一个保存上下文数据的字段。

    在这里插入图片描述

    3.环境变量

    3.1常见环境变量
    PATH : 指定命令的搜索路径 [重点]
    HOME : 指定用户的主工作目录(即用户登陆到Linux系统中时,默认的目录)[重点]
    SHELL : 当前Shell,它的值通常是/bin/bash
    
    • 1
    • 2
    • 3

    查看环境变量

    env $NAME //NAME:你的环境变量名称
    1. echo: 显示某个环境变量值[重点]
    2. export: 设置一个新的环境变量[重点]
    用法:export 本地变量名
    3. env: 显示所有环境变量[重点]
    4. unset: 清除环境变量
    5. set: 显示本地定义的shell变量和环境变量
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    3.2环境变量PATH

    可执行程序的搜索路径是保存在一个全局变量中,PATH,给系统提高命令的搜索路径。是环境变量中的一个

    $ echo $PATH
    /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/root/bin
    #每个目录中间使用冒号隔开。
    #PATH的搜索顺序:从左向右依次进行程序的搜索,找到就执行程序,找不到就找下一个目录。如果全部都没有,就报错:command not found!
    #也就是执行路径下的可执行文件可以直接使用(可以不指出相应的路径)
    
    • 1
    • 2
    • 3
    • 4
    • 5

    向PATH中添加路径

    #方法一:
    $ PATH="${PATH}:/root"
    #在该用户下的环境变量中添加路径/root
    #方法二:
    $ export PATH=$PATH:/root
    #在该用户下的PATH环境变量中添加路径/root
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    命令行变量

    • 普通变量
    • 环境变量(全局)
    3.3环境变量的C/C++获取方式

    main函数的参数

    int main(int argc,char* argv[])
    {
        /*
        argc:参数个数
        argv:指针数组,并且第一个字符串指向的是这个可执行程序的名字,最后一个字符串指向的是NULL 
        */
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 给main函数传递的argc,char* argv[],传递的是命令行参数的个数和选项。
    • argv[]中第一个指向的是程序名,最后一个指向的是NULL。
    • main函数还可以传递第三个参数char env[ ]----->环境变量*

    **实现命令行计算器 **

    #include
    #include
    #include
    
    int main(int agrc,char* argv[])
    {
        if(argc!=4)
        {
            printf("Useage:[-a][-s][-m][-d]\n");
        }
        int x=atoi(argv[1]);
        int y=atoi(argv[2]);
        if(strcmp("-a",argv[3])==0)
        {
            printf("%d-%d=%d\n",x,y,x-y);
        }
        else if(strcmp("-s",argv[3])==0)
        {
            printf("%d+%d=%d\n",x,y,x+y);
        }
        else if(strcmp("-m",argv[3])==0)
        {
            printf("%d*%d=%d\n",x,y,x*y);
        }
        else if(strcmp("-d",argv[3])==0)
        {
            printf("%d/%d=%d\n",x,y,x/y);
        }
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    在这里插入图片描述

    获取环境变量
    • 通过main函数的第三个参数获取
    遍历环境变量
    #include 
    int main(int argc, char *argv[], char *env[])
    {
    	int i = 0;
    	for(; env[i]; i++){
    		printf("%s\n", env[i]);
    	}
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    在这里插入图片描述

    • 通过第三方变量environ获取
    #include 
    int main(int argc, char *argv[])
    {
    	extern char **environ;
    	int i = 0;
    	for(; environ[i]; i++){
    		printf("%s\n", environ[i]);
    	}
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 系统调用:getenv(环境变量名)
    #include
    int main()
    {
    	printf("%s\n", getenv("PATH"));
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    实现一个只能被本用户调用的程序

    int main()
    {
        char * var=getenv("USER");
        if(strcasecmp(var,"west")!=0)
        {
            printf("没有权限...........\n");
        }
        else
        {
            printf("执行成功.........\n");
        }
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    一个函数声明的时候没有带参数,可以传参吗?

    int fun()
    {
    	printf("hello world\n");    
    }
    int main()
    {
        fun(10,20,30,40);
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    答案是可以传递参数。但是不能写成 fun(void),这就表明函数一定不能传递参数。

    3.4本地变量和环境变量
    • 环境变量的全局性:环境变量会被子进程继承下去。
    • 本地变量:本质是在bash内部定义的变量,不会被子进程继承下去
    • **内建命令:**Linux下大部分命令都是通过创建子进程执行,但是有一部分特殊的命令,是在bash执行(调用bash内部的函数实现特点的功能)

    在这里插入图片描述

    4.进程地址空间

    每一个进程启动,操作系统都会给进程创建一个地址空间,每一个进程都有自己的进程地址空间。

    操作系统为了管理这些进程地址空间,有数据结构mm_struct进行管理【管理该进程的进程地址空间】。

    image-20220813173812934

    4.1进程的基本概念

    进程的独立性:体现在进程相关的数据结构是独立的,代码和数据是独立的。

    4.2MMU(内存管理单元)的作用

    **在32位机器下,一个进程可以管理的虚拟内存空间大小为4G。**而实际上的物理空间大小不是4G。而物理内存和虚拟内存自己的对应和管理就是通过MMU和地址转换记录表进行。

    image-20220705021709810

    比如在.data中存放了a=10,那么就可以通过MMU(记录了虚拟内存和物理内存的映射关系),将他的虚拟地址转换为物理地址,从而访问内容10。

    内存访问级别:

    • 0是最高的级别,内核访问的权限

    • 3是允许用户访问权限级别的权限

    映射问题:

    • 用户空间映射到物理空间内存是独立的(不同的进程,即使虚拟地址相同,对应的也是不同的物理空间)

    • 内核空间映射到物理空间内存是相同的。(上图中,两个进程的kernal指向了同一块权限为0的MMU)

    4.3页表

    页表------将进程地址空间和物理地址映射【左边是虚拟地址,右边是物理地址】

    • 页表:也叫转换表(Translation Table).它负责MMU虚拟地址与物理地址之间的映射关系,由用户编写并存放在内存中。这样就可以将MMU与页表联系起来,进行转换工作了。页表由很多个页表项组成。

    在这里插入图片描述

    相对地址/逻辑地址/进程地址空间 --------> 映射物理地址

    4.4读时共享写时拷贝原则
    int val=100;
    int main()
    {
        pid_t pid=fork();
        if(pid==0)
        {
            int i=5;
            while(i)
            {
                printf("I am father pro,pid=%d,val=%d",getpid(),val);
                sleep(1);
                i--;
            }
            val=200;
            printf("begin chage................\n");
            while(1)
            {
                printf("I am child pro=%d,val=%d\n",getpid(),val);
                sleep(1);
            }
        }
        else
        {
          while(1)
          {
              printf("I am father pro,pid=%d,val=%d\n",getpid(),val);
              sleep(1);
          }
        }
       return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    在这里插入图片描述

    为什么进程地址一样,但是内容确不一样。

    写时拷贝原则:

    image-20220813172853055

    实现了进程的独立性

    为什么fork()有两个返回值?

    pid_t pid是属于父进程空间定义的变量,return返回的时候,pid发生了写时拷贝,所以有两个返回值。

    4.5为什么有进程地址空间
    • 指针越界的问题,保护内存安全
    • 进行内存管理,提高运行效率。
    • 对功能模块进行解耦

    在这里插入图片描述

  • 相关阅读:
    CSV 文本过滤查询汇总计算
    SimpleITK学习笔记
    MySQL日志管理和权限管理(重点)
    Nacos - 配置中心&注册中心
    14.梯度检测、随机初始化、神经网络总结
    简单的自定义滚动条
    达梦数据库之 PERCENT_RANK()over()函数和 PERCENTILE_CONT() WITHIN GROUP()OVER()函数详解
    以 ZGC 为例,谈一谈 JVM 是如何实现 Reference 语义的
    第七章·原型模式
    Genio 500_MT8385安卓核心板:功能强大且高效
  • 原文地址:https://blog.csdn.net/qq_53893431/article/details/126446084