【Linux】进程基础

本文详细介绍了冯诺依曼体系结构,包括CPU、内存和外设的关系,以及为何需要内存。接着深入探讨了操作系统的概念,特别是进程管理,包括进程的状态(如运行、阻塞、挂起和终止)、task_struct数据结构、进程优先级及其在Linux下的实现。还讨论了环境变量的使用和获取方式,以及进程地址空间和MMU的角色。最后,阐述了进程地址空间的重要性,如保护内存安全和提高运行效率。
摘要由CSDN通过智能技术生成


在这里插入图片描述

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

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 */[重点]
};

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;
}

在这里插入图片描述

int main()
{
    printf("begin.....\n");
    while(1)
    {
        printf("hello world\n");
    }
    return 0;
}
ps ajx | head -1 && ps ajx | grep hello

在这里插入图片描述

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

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

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

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

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

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

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中。

在这里插入图片描述

维护方式:

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

进程切换

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

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

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

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

在这里插入图片描述

3.环境变量

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

查看环境变量

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

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

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

向PATH中添加路径

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

命令行变量

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

main函数的参数

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

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

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

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;
}

在这里插入图片描述

获取环境变量
  • 通过main函数的第三个参数获取
遍历环境变量
#include <stdio.h>
int main(int argc, char *argv[], char *env[])
{
	int i = 0;
	for(; env[i]; i++){
		printf("%s\n", env[i]);
	}
	return 0;
}

在这里插入图片描述

  • 通过第三方变量environ获取
#include <stdio.h>
int main(int argc, char *argv[])
{
	extern char **environ;
	int i = 0;
	for(; environ[i]; i++){
		printf("%s\n", environ[i]);
	}
	return 0;
}
  • 系统调用:getenv(环境变量名)
#include<stdlib.h>
int main()
{
	printf("%s\n", getenv("PATH"));
	return 0;
}

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

int main()
{
    char * var=getenv("USER");
    if(strcasecmp(var,"west")!=0)
    {
        printf("没有权限...........\n");
    }
    else
    {
        printf("执行成功.........\n");
    }
    return 0;
}

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

int fun()
{
	printf("hello world\n");    
}
int main()
{
    fun(10,20,30,40);
    return 0;
}

答案是可以传递参数。但是不能写成 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;
}

在这里插入图片描述

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

写时拷贝原则:

image-20220813172853055

实现了进程的独立性

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

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

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

在这里插入图片描述

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

影中人lx

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值