【Linux】系统进程的概念

一、冯诺依曼体系结构

         

         我们常见的计算机比如常见的笔记本,服务器大部分遵循冯诺依曼体系结构。

         电脑由一个个硬件组成,硬件接受到的数据传入给存储器,存储器交给CPU进行计算,计算后的结果返回给存储器,然后再由存储器把数据传给输出设备。

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

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

存储器:内存。

CPU:运算器进行算术运算、逻辑运算。

        那么这里就问题来了,为什么不直接让CPU与输入输出设备进行交互,要在中间加一个内存呢?

        首先我们要知道每个硬件对数据的反应速度是不同的,由高到低为 CPU && 寄存器 > 内存 > 磁盘/SSD > 光盘 > 磁盘

        然后要知道木桶效应,一个桶能装多少水,不是由最长的一块板来决定的,是根据最短的一块板来决定的。

       CPU运算速度快,外部设备的速度慢,一边接受数据,一边计算,这样会照成整体效率变低,加一个内存,把接受到的数据到一定程度后再送给CPU计算然后返回,能有效的提高效率。且这种结构效率高,成本低。

PS:

1.CPU读取数据(数据+代码),都是要从内存中读取。站在数据的角度,我们认为CPU不和外设直接打交道。

2.CPU要处理数据,需要先将外设中的数据,加载到内存。站在数据的角度,外设直接和内存打交道。

        这里我们就知道了程序要运行,就必须加载到内存中,这是其体系结构决定的。

二、操作系统

        操作系统很复杂,是单一的一门学科,学习C++编写程序,暂时不用了解太深,但是要注意我们自己写的程序也都是操作系统在管理。

        操作系统本质其实就是一个对软硬件资源做管理的软件

        操作系统是分层次的,平时我们用户使用操作系统的时候,使用的是操作系统专门给外部调用的接口,而内部很多核心的东西是自己再进行管理,不会让用户接触。

        这是为了用户提供一个稳定,安全,简单的执行环境。

那么操作系统如何管理数据呢?

      1.先描述 ( 使用struct结构体 )

      2.再组织 ( 使用数据结构排序结构体 )        

PS:这里使用的是struc来描述数据,而不是class,因为Linux或Windows其大多数核心内容是C语言加汇编写的。

三、进程 

        其实我们自己启动一个软件,本质其实就是启动了一个进程!在Linux中,运行一条命令, ./XXX,运行的时候,其实就是在系统层面创建了一个进程。

        Linux是可以同时加载多个程序的,Linux是可能同时存在大量进程在系统中(OS,内存)

Linux系统配合是如何管理大量的进程的呢?

        还是先描述,再组织。

        为了管理进程,操作系统为每一个进程申请了一个结构体PCB,包含了进程所有属性。(属性和程序的代码和数据没有关系)

        对进程的管理,就变成了对进程PCB结构体链表的增删改查。

所以进程就是:程序的代码和数据 + 对应的 PCB 结构体

        不同的操作系统中,PCB(process control block) 的名字不同。Linux下的进程结构体名为task_struct。

task_struct结构中当中主要包含以下信息:

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

上下文数据:

        每个运行的进程,都有自己的“时间片” ,进程调度是基于时间片的轮转。可能在任何时间点进程被切换,如果这里进程被切走了,下一次切回进程时如何保证回到刚刚的状态呢?

        进程A正在运行,cpu内的寄存器保存的,一定是A的临时数据。

        寄存器中的临时数据,叫做A的上下文。进程A暂时被切下来的时候,需要A顺便带走自己的上下文数据! 带走的目的就是为了下次回来的时候,能恢复上去,就能继续按照之前的逻辑继续向后运行,就如同没有中断过一样。

        而这里进程带走上下文数据后就保存在task_struct的结构体里面。

        在根目录下有一个名为proc的系统文件夹 , 文件夹当中包含大量进程信息,其中有些子目录的目录名为数字 , 这些数字其实是某一进程的PID,对应文件夹当中记录着对应进程的各种信息

进程相关指令:

ps axj:显示系统中所有的进程。

        这里PID是进程ID,PPID是父进程ID。

cwd:当前进程的工作目录

kill -9 + 进程ID:杀死一个进程

    kill后面可以跟的命令有很多,使用指令kill -l可以查看全部的命令。

getipid() //获取自己进程的id

getppid() //获取父进程的pid

pid_t fork() //创建一个子进程

//失败返回-1,成功:a.给父进程返回子进程的pid	b.给子进程返回0
#include<stdio.h>
#include<unistd.h>
                                                                                
int main()
{
     pid_t id = fork();
     if(id < 0)
    {
      perror("fork");
      return 1;
    }
    else if(id == 0)
    {
      while(1)
      {
        printf("i am child ,pid:%d,ppid:%d\n",getpid(),getppid());
        sleep(1);
      }
    }
    else 
    {
      while(1)
      {
        printf("i am father,pid:%d,ppid:%d\n",getpid(),getppid());
        sleep(1);
      }
    }
    printf("I am a process !");
    sleep(1);
    return 0;
}       

   

ps axj|head -1 && ps ajx|grep mytest //监控文件进程

while :; do ps axj|head -1 && ps ajx|grep mytest |grep -v grep; sleep 1;echo "####################################"; done //持续监测进程

       

        这里程序自己创建了一个进程,并且调用函数pid_t fork(),创建了一个子进程。

PS:父子进程代码共享,数据各自开辟空间,杀死父进程并不影响子进程。

PS:每个进程重新开始运行的时候,系统都会重新分配PID。

        为什么 fork() 给子进程返回0,给父进程返回子进程的pid,这是因为一个子进程只有一个父进程,一个父进程可以有多个子进程。

        这里要说明一下为什么 fork() 能返回两个变量,这是因为子进程在函数内部生成,在return id的时候,已经创建了子进程了,父子进程执行各自的return,那么return会被执行两次,所以返回两个值。

进程状态:

        操作系统调度进程时,并不是同时一起运行,而是哪个需要就先运行哪个进程,那么此时进程就会有多种状态。

        而这里切换进程使用的是运行队列来控制。运行队列容纳了系统中所有可以运行的进程,它是一个双向循环队列。该队列通过 task_struct 结构中的两个指针run_list链表来维持。

//task 结构体里面表示状态的几个变量
static const char* task_state_array[]=
{
     R,//0	运行状态   
     S,//1 	阻塞状态 也叫可以中断睡眠  
     D,//2  磁盘睡眠,深度睡眠,不可被中断,不可以被被动唤醒
     T(stopped),//4  暂停状态   
     t,(tracing stop)//8  调试暂停
     Z,//16    僵尸状态   
     X (dead),//32 终止 非常快 表示当前资源可以被回收 
}

        运行:进程在运行队列中排队或者正在运行,就叫做运行态。

        阻塞:等待非CPU资源就绪,叫阻塞状态。

int a = 0;
scanf("%d",&a);

        这段C语言如果不输入,那么程序会一直等待,这里就是等待键盘数据,那么这里就是阻塞状态。
        深度睡眠:为了防止进程在写入磁盘数据等待返回时,进程被操作系统杀掉。某一进程要求对磁盘进行写入操作,那么在磁盘进行写入期间,该进程就处于深度睡眠状态,是不会被杀掉的,因为该进程需要等待磁盘的回复(是否写入成功)以做出相应的应答。

        暂停状态 (T):进程的暂定,可以使用指令,kill -19 + 进程ID 暂停进程,使用 kill -18 + 进程ID 继续运行进程。

        调试暂停 (t):最常用在调试的时候,gdb这里b打断点,运行到断点位置停下来,就是进程暂停。        

        僵尸状态:是一个比较特殊的状态。当进程退出并且父进程没有读取到子进程退出的返回代码时就会产生僵尸进程,僵尸进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码。所以,只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程进入Z状态

        维持该状态是为了让父进程或者操作系统回收。

        死亡状态:只是一个返回状态,当一个进程的退出信息被读取后,该进程所申请的资源就会立即被释放,该进程也就不存在了,所以你不会在任务列表当中看到死亡状态。

        这里使用查看进程信息的命令,也能看到进程的状态,这里有个 + 号表示是前台进程,不能在运行的时候输入命令。

        没有则表示后台程序,能执行命令行,不能Ctrl+C中断程序,用 kill -9 关闭程序。

./mytest f //前台程序运行
./mytest & //后台程序运行

孤儿进程:

        父进程退出,子进程还在的进程,子进程就叫孤儿进程。

        孤儿进程会被领养,被1号进程(init,系统本身)领养。

进程优先级:

        CPU是有限的,进程太多,需要某种方式来竞争资源。

        优先级实际上就是获取某种资源的先后顺序,而进程优先级实际上就是进程获取CPU资源分配的先后顺序。

ps -l //使用指令查看优先级

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

Linux具体的做法:

        优先级 = 老旧的优先级 + nice值

PS:老的优先级,是进程最开始的优先级,而不是指上一次的优先级。

使用top指令更改已存在进程的nice:

        

       出现界面后按"r"键,输入PID,输入nice值更改。建议不要随便跟改程序的优先级。

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


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

四、环境变量

       

        像我们平时运行程序的时候,需要去他自己的目录下运行,但是使用系统自带的命令是就可以直接使用,这是因为其默认配置了环境变量,如果你为你的程序也配置了环境变量,那么你也可以随便在什么位置运行。

echo $PATH    //查看环境变量

设置环境变量:

export PATH=$PATH: + 软件的绝对路径

         发现这里只需要输入名字就可以直接运行。

env    //查看全部环境变量

 PS:命令行里面改环境变量,只能在本次会话生效。改配置文件才能长久生效。

拓展:过代码获取环境变量

main函数可以带三个参数:

  • argc:int 类型,用于存放命令行参数的个数(包括函数名)。
  • argv[]:char数组型,每个元素都是一个字符指针,指向一个字符串,即命令行中的每一个参数。
  • envp:char数组型,这个数组的每一个元素是指向一个环境变量的字符指针,也就是存放了当前程序运行时的环境变量(当前程序运行时对应的进程包含的环境变量)。
int main(int argc,char* argv[],char* env[])
{
	//1.通过第三个参数
    for(int i=0;env[i];i++){
        printf("%s\n",env[i]);
    }

}

//2.通过第全局变量environ
extern char**environ;

for(int i=0;environ[i];i++){

    printf("env[%d]:%s\n",i,environ[i]);

}

//3.库函数 获取指定的环境变量
getenv("PATH"); 

printf("%s\n",getenv("PATH"));

PS:环境变量的程序的父进程传入进来的。

int main(int argc,char *argv[])
{
    if(argc!=2){
        printf("Usage:%s 至少要一个选项\n,",argv[0]);
    }
    
    if(strcmp("a",argv[0])){
        printf("这是功能一\n");
    }
    else if(strcmp("b",argv[0])){
        printf("这里功能二\n");
    }
}

        这里就简单的实现了 不同的选项,执行不同的功能。


猜你喜欢

转载自blog.csdn.net/weixin_45423515/article/details/126808641