0


【Linux】进程

本文主要介绍了进程的相关理解:查看进程、进程状态、进程的优先级、环境变量、进程地址空间、Linux内核进程调度队列。

冯诺依曼体系结构

截止目前,我们所认识的计算机,都是由诸多硬件组成:

输入设备:键盘、显示器、摄像头、话筒、磁盘、网卡

输出设备:显示器、声卡、磁盘、网卡

CPU:运算器、控制器

存储器:内存

关于冯诺依曼体系结构,有这样的经典示意图:

图中的箭头可以看做数据的流向,数据是要在计算机的体系结构中进行流动的,在流动过程中,进行数据的加工处理,数据从一个设备到另一个设备,在本质上是一种拷贝!

数据设备间的拷贝效率,决定了计算机整机的效率。

关于冯诺依曼结构,需要强调几点:

1.CPU不和外设打交道,只和内存打交道。

2.外设输入输出的数据,不是直接给CPU,而是先要放到内存中

操作系统

操作系统就是一款软件,对软硬件资源进行管理。

对操作系统广义的认识:操作系统的内核+操作系统的外壳周边程序(给用户提供使用操作系统的方式)

对操作系统狭义的认识:只是操作系统的内核

为什么要有操作系统?

提供对软硬件资源进行管理(手段),为用户提供一个良好(稳定、安全、高效)的运行环境(目的)。

这里要引入一个非常重要的理念,任何管理(包括操作系统)都要遵循:

先描述,再组织!

这里的描述是指对管理对象进行描述,例如,在之前写通讯录时,先要定义一个包括各种联系人信息的结构体,然后使用数组这种数据结构进行组织。

在上图中,可以看到,操作系统为上面用户操作提供了系统调用接口,用户不能直接改变操作系统里的内容。

系统调用和库函数概念

在开发角度,操作系统对外会表现为一个整体,但是会暴露自己的部分接口,供上层开发使用,这部分由操作系统提供的接口,叫做系统调用。

系统调用在使用上,功能比较基础,对用户的要求相对也比较高,所以,有心的开发者可以对部分系统调用进行适度封装,从而形成库,有了库,就很有利于更上层用户或者开发者进行二次开发。

操作系统管理硬件

1.使用struct结构体对硬件描述

2.使用链表或其他数据结构组织起来

进程

所谓进程,是程序执行的一个实例,正在执行的程序。从内核观点看,进程是担当分配系统资源(CPU时间,内存)的实体。

进程信息被放在一个叫做进程数据块的数据结构中,是进程属性的集合。一般称之为PCB(process control block),Linux操作系统下的PCB是:task_struct。

当加载一个程序时,对应的代码和数据被加载到内存中,但是代码和数据不是进程,只是进程的一部分,进程还包括对应的PCB。

存在PCB的原因是:OS要对进程进行管理,先描述,再组织。这样对进程的管理,就变成了对链表的增删查改!

总结一下,进程=PCB+自己的代码和数据,对Linux操作系统来说,进程=内核task_struct结构体+程序的代码和数据。

理解一个概念:如何理解系统动态运行?

调度运行进程,本质就是让进程控制块task_struct进行排队!

查看进程

几点预备小知识

a)./xxxx,本质就是让系统创建进程并运行,我们自己写的代码形成的可执行文件=系统命令=可执行文件,在linux中的大部分执行操作,本质都是运行进程!


b)每一个进程都要有自己的唯一标识符,叫做进程pid

有myprocess.c这样一个文件,运行后生成myprocess的可执行文件。

#include <stdio.h>
#include <unistd.h>
int main()
{
  while(1)
  {
    printf("I am a process!\n");
    sleep(1);
  }
  return 0;
}

复制当前窗口,在一个窗口运行myprocess可执行程序(./myprocess),在另一个窗口输入下面的指令:

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

这条指令由&&两边的两条指令组成,&&的意思是执行两条指令,其左边:

ps ajx | head -1

这句代码是打印出进程的标题行,

其右边:

ps ajx | grep myprocess

打印出包含‘myprocess’关键字的进程。

第二列就是进程的pid,pid的类型是unsigned int pid。


c)可以使用getpid()得到进程的pid

如果不想通过ps ajx命令去查进程的pid,那么还可以使用getpid()函数去查,

如上图所示,每个进程都有自己对应的pid。pid属于内核数据结构中的数据,因此用户不用直接获得,需要调用系统接口。


d)Ctrl+C是在用户层面终止进度,kill -9 pid可以用来直接杀掉进程

进程创建的代码方式

首先看一个指令:

pid getppid(void); ---获取当前进程的父进程id

从上图发现,每次启动进程后,对应地 pid都不一样,这是正常的,但是对应的ppid(父进程)是一样的!上图的父进程都是6046,于是我们很好奇谁是6046啊?

我们可以通过如下的代码查到:

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

原来6046是bash,bash就是父进程,是命令行解释器!


接下来,我们在代码中创建子进程:使用fork()命令

fork后的指令由父子进程共享!

1.创建一个进程,本质就是系统中多了一个进程;多了一个进程,就是多了1和内核task_struct。

2.多了的一个进程,有自己的代码和数据,父进程的代码和数据是从磁盘加载来的,而子进程的代码和数据默认情况继承父进程的代码和数据(代码由于是只读的,所以父子共用一份代码,而数据可读可写,父子各自独立,原则上要分开!)。

为什么要创建子进程

想让子进程和父进程执行不一样的代码!

看下面这段代码:

可以看到,当id不同时,进入不同的代码,这就是多进程情况。其中,fork是一个函数,由操作系统提供,fork会返回两次。为什么会fork返回两次呢?前面说到,fork后的代码由父子进程共享,其实这样说并不准确,在fork函数内部的return语句前,子进程已经被创建,因此,return语句也是由父子进程共享,会被父子进程各执行一次,会返回两次!

需要注意的是,进程间一定要做到:进程具有独立性!杀掉父进程不会影响子进程,杀掉子进程不会影响父进程!

样例代码:依次创建多个进程

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
void RunChild()
{
  while(1)
  {
    printf("I am child,pid:%d,ppid:%d\n",getpid(),getppid());
    sleep(1);
  }
}
int main()
{
   const int num = 5;
   int i=0;
   for(i=0;i<num;i++)
   {
     pid_t id = fork();
     if(id == 0)
     {
       RunChild();
     }
   }
   while(1)
   {
     sleep(1);                                                      
     printf("I am parent,pid:%d,ppid:%d\n",getpid(),getppid());
   }
    return 0;
}

如上,一次创建了5个进程!


除了使用ps ajx命令查看进程外,在/proc系统文件夹中也可以查看进程信息,例如,我当前正在运行pid为11129的进程,查看/proc文件夹

可以找到名称为11129的文件夹,打开11129文件夹,

其中,cwd(current work dir)保存了当前路径,这也就是我们在使用

fopen("log.txt","w");

这个命令时,为什么可以在当前路径下创建!

exe:进程的pcb会记录自己对应的可执行程序的路径。

每个进程在启动的时候,会记录自己当前在哪个路径下启动,就是进程的当前路径,当用上面的fopen创建log.txt时,会把当前路径cwd和log.txt拼接(cwd/log.txt)

此外,我们还可以通过chdir改变进程的路径:

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
   chdir("/home/ghs/Linux");
   FILE* fp = fopen("log.txt","w");
   fclose(fp);
   (void)fp;//ignor warning
   while(1)
   {
     printf("I am a process,pid:%d\n",getpid());
     sleep(1);                                                              
   }
   return 0;
}

通过chdir("/home/ghs/Linux");这行代码将该进程的工作路径改到/home/ghs/Linux下,

通过查看进程的pdb,发现进程路径确实已经改变!

进程状态

Linux的进程状态

进程状态,就是内核数据结构struct task_struct内部一个 属性,一个进程可以有几个状态:

R:进程运行状态

S:休眠状态,可中断睡眠,进程在等待“资源(显示器,网卡等)”就绪

T/t:让进程暂停,等待被进一步唤醒,T-纯粹的暂停,t-遇到断点处进程暂停

D:深度睡眠状态,不可被杀,不可中断,在这种状态下,要么进程自己醒来,要么重启(断电),是Linux系统比较特有的一种进程状态。

X:死亡状态

Z:僵尸状态

这里补充一点关于kill的指令:

kill -l   --- 查看kill的所有命令
kill -9 pid  --- 杀掉进程
kill -19 pid  --- 暂停进程
kill -18 pid  --- 继续进程

实际上 ,我们之前都用过暂停进程-打断点。

僵尸进程和孤儿进程

僵尸进程(Z):已经运行完毕,但是需要维持自己的退出信息,在自己的进程task_struct会记录自己的退出信息,未来让父进程进行读取,如果没有父进程,僵尸进程会一直存在!

进程退出时,进程的代码和数据会被释放掉,但是进程的task_struct会一直被维持住,直到等待父进程来读取,如果一直不读取,那就会出现内存泄漏问题。

孤儿进程:父进程如果先退出,子进程就会变成孤儿进程,孤儿进程一般都会被1号进程(OS本身)进行领养的。孤儿进程要被领养的原因是:依旧要保证子进程正常被回收。

但是,在我们已经启动的所有进程中,怎么从来没有关心过僵尸进程呢?内存泄漏??

原因:直接在命令行中启动的进程,它 的父进程是bash,bash会自动回收新进程的Z!

进程的运行、阻塞和挂起

运行: 进程在运行队列中,该进程的状态就是R状态。

一个进程一旦持有CPU,会一直运行到这个进程结束吗??

不会! 当代内核是基于时间片进行轮转调度的,每个进程都有一定的时间片,即使进程为执行结束,只要时间片一到,会自动把这个进程从CPU上剥离下来,再从新列入到运行队列的尾部,让下一个进程再来进行CPU的调度,按照这中方式,让所有进程在一定时间内都得到调度。让多个进程以切换的方式进行调度,在一个时间段内同时得以推进代码,就叫做并发。但是,Linux不是这样调度的!只是OS教材调度算法的一种。

另外,任何时刻,都同时有多个进程在真的同时进行,叫做并行

阻塞态:等待,等待键盘资源是否就绪,键盘上有没有被用户按下的按键,按键数据交给进程。

不是CPU才有运行队列,各种设备也有自己的wait_quene,当CPU中运行队列的某个进程需要等待键盘资源时,这个进程需要被放到设备的wait_quene,该进程就绝对不会被CPU调度了,把这种进程叫做阻塞。当从键盘上输入后,进程就会从设备的wait_quene重新链入CPU的运行队列中,之后该进程再次被调度时,会继续进程该进程的后续代码。

阻塞和运行的状态变化,往往伴随着pcb被链入不同的队列中!入队列的不是进程的代码和数据,而是进程的task_struct。

挂起态:当OS内存特别吃紧时,OS会将一些S状态的进程对应的代码和数据放到磁盘上的swap分区(挂起到外设),这样就会为OS腾出一定的内存空间,以解决燃眉之急,当该进程需要被调度时,再把对应的代码和数据换到内存中,这样,操作系统在辗转腾挪之间更合理地使用内存资源(其实这种属于阻塞挂起)。

** 关于进程切换的话题**:

假设有两个进程,分别是task_struct1和task_struct2,当task_struct1的时间片到了,需要切换到task_struct2,CPU的寄存器会保存1进程的临时数据。CPU内部的所有寄存器中的临时数据,叫做进程的上下文。进程在切换时,最重要的一件事是:上下文数据的保护和恢复

CPU内的寄存器:寄存器本身是硬件,具有数据的存储能力,CPU的寄存器硬件只有一套!!

CPU内部的数据,可以有多套,有几个进程,就有几套和该进程对应的上下文数据。

寄存器!=寄存器内容

进程的优先级

概念

指定进程获取某种资源的先后顺序。

进程是用task_struct结构体来描述的进程控制块,这个结构体有很多内部字段,优先级就是就是结构体内的一个字段(int prio,一个整数或若干个整数),优先级的本质就是一个数字。Linux中,优先级数字越小,优先级越高。

下面我们区分一组概念:优先级 vs 权限

权限:能不能获取某种资源

优先级:已经能了,我们获取资源的顺序是什么

为什么要有优先级?

答:进程访问的资源(CPU)始终都是有限的,而系统中进程大部分情况都是有较多的。

操作系统关于调度和优先级的原则:分时操作系统,保证基本的公平。如果进程因为长时间不被调度,就造成了饥饿问题。

Linux的优先级的特点&&查看方式

在Linux系统中,用ps -al命令查看进程优先级,会输出一下几个内容:

其中的PRI就代表进程的优先级,NI表示进程优先级的修正数据,nice值。

新的优先级=优先级+nice,达到对于进程优先级动态修改的过程。

nice值并不能让你任意调整,而是有范围的!是[-20,19],一共40个数字。

每次调整优先级,都是从80开始。

其他概念

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

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

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

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

命令行参数

我们来想一个问题,我们经常写的main函数需要带参吗?main函数既然是函数,那么谁来调度main函数呢?

实际上,main函数有参数:

int main(int argc,char* argv[])

main函数的参数可带可不带。但是,这次要讨论的问题是要带上这些参数(int argc,char* argv[])的意义及用法:

argv是一个字符指针数组,其中每一个元素都指向一个字符串,agrc是argv数组的元素个数

为了具体看一下argv指向的字符串,我们设计如下代码:

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

int main(int argc,char* argv[])
{
  size_t i=0;
  for(i=0;i<argc;i++)
  {
    printf("argv[%d]:%s\n",i,argv[i]);
  }

  return 0;
}

上面我们在命令行输入的叫做命令行字符串,argv是一个变长的数组,其长度和命令行字符串中的元素个数有关。在命令行中,虽然收到“./myprocess -a -b -c -d”这一行字符串,但是系统会帮助我们把这个字符串打散,把这个字符串中的空格设置成‘\0’,

以上我们了解了命令行参数的概念,可是现在有两个问题我们比较关心:

1.为什么要这么干?

本质:命令行参数本质是交给我们程序的不同的选项,用来定制不同的程序功能,命令行会携带很多的选项。如下面的代码:

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

int main(int argc,char* argv[])
{
  if(argc != 2)
  {
    printf("Usage:%s -[a,b,c,d]\n",argv[0]);
    return 1;
  }
  if(strcmp(argv[1],"-a")==0)
  {
    printf("this is function1\n");
  }
  else if(strcmp(argv[1],"-b")==0)
  {
    printf("this is function2\n");
  }
 else if(strcmp(argv[1],"-c")==0)
  {
    printf("this is function3\n");
  }
  else if(strcmp(argv[1],"-d")==0)
  {
    printf("this is function4\n");
  }
  else
  {
    printf("no this function\n");
  }
  return 0;
}

2.这些工作时谁干的?

#include<stdio.h>
#include<unistd.h>
int g_val=10000;
int main()
{
  printf("I am father process,pid:%d,ppid:%d,g_val:%d\n",getpid(),getppid(),g_val);
  sleep(5);

  pid_t id = fork();
  if(id == 0)
  {
    //child
    while(1)
    { 
      printf("I am child process,pid:%d,ppid:%d,g_val:%d\n",getpid(),getppid(),g_val);
      sleep(1);
    }
  }
  else
  {
    //father
    while(1)
    { 
      printf("I am father process,pid:%d,ppid:%d,g_val:%d\n",getpid(),getppid(),g_val);
      sleep(1);
    }
  }

  return 0;
}

从上面这段代码中,可以看到,子进程也可以看到父进程g_val的值,因此,可以得出结论:

父进程的数据,默认能被子进程看到并访问!

在上面这段代码中,父进程的ppid是431,这个431其实是bash(之前也说过),因此,另一个结论:

命令行中启动的程序,都会变成进程,其实都是bash的子进程!

因此,我们在命令行中输入的字符串“./myprocess -a -b -c -d”其实默认是输入给父进程bash(命令行解释器)的!!因此,父进程bash(命令行解释器)要对后面输入的命令行字符串进行解释,即将命令行字符串作为参数传给main函数的argv参数,因此,是父进程bash做的这些工作

环境变量

在执行ls -a等这些命令时(本质是一段程序,见下图),本质和运行我们自己的./myprocess一样,我们自己也可以带上-a选项,

但是凭什么ls -a运行时不需要带上路径,而我们自己运行的程序需要带上./这样的路径?

原因就在于,Linux中,存在一些全局的设置,表明,告诉命令行解释器,应该去哪些路径下去寻找可执行程序。

PATH:环境变量。如果想要打印环境变量的内容,需要用$PATH打印,环境变量有很多,PATH只是一个环境变量。系统中的很多配置,在我们登录Linux系统的时候,已经被加载到bash进程中(内存)

这个环境变量中,我们以:为分隔符,由:划分出来的一块区域是一个路径。

bash在执行命令的时候,需要先找到命令,因为未来要加载!那么去哪找呢?bash维护了一批路径(如PATH),这批路径就是在bash在执行命令时默认的搜索路径。比如:在执行ls命令时,需要在上面的一批路径下去找,第一个路径下没找到就要去第二个路径下去找,依次类推,如果全都没找到,那么就报错(command not found ),找到了就加载并运行这个程序,显示程序执行的结果。总结一下,执行ls不用带路径的原因是ls所在的usr/bin路径是在PATH环境变量当中的

那么今天我们有一个需求:我想执行我们的命令,和系统指令一样,怎么做呢?

我们可以简单粗暴地把我们命令拷贝到PATH环境变量中,

其实,这就相当于我们把myprocess安装到系统里,但是强烈不推荐这样做,污染了别人的指令集

所以,我们把刚才复制的myprocess删掉,相当于卸载!

我们还可以温柔一点,不要直接把myprocess安装到系统里:由于PATH是一个环境变量,那么我么可以直接对其赋值,如下:

但是,这样相当于把PATH的内容进行了覆盖,导致其他的大部分系统指令不能用了,因此,决不能这么干!!!(如果这么做了也没关系,重新登录系统即可,原因就是我们这种环境变量只是内存级的)

那么,为了把我们命令的路径添加到环境变量中而不覆盖原有的路径,可以这样:

PATH=$PATH:/home/ghs/Linux/linux/lesson14

可以看到,myprocess的路径已经被加到PATH环境变量里了。

$PATH:表示在原有路径后面添加,而不覆盖之前的。

这样,无论带不带./都可以执行了,myprocess可以在PATH环境中搜索路径!

可是,现在修改后的PATH是内存级的,如果让它永久有效怎么办呢?

我们需要知道,最开始的环境变量不是在内存中,而是在系统的对应的配置文件中。这个配置文件在那里呢?使用cd ~回到用户家目录,ls -la,会有.bash_profile和.bashrc两个文件,在登录linux系统时,会将这两个文件里的内容加载到bash进程中,环境变量默认是在配置文件中的

打开.bash_profile文件,

那么,我们将.bash_profile中的PATH修改,就可以在每次登录系统时加载我们修改过的PATH,如下图:

然后关闭Xshell,重新登录即可(修改的.bash_profile,需要使用 su - 用户名 登录):

常见环境变量

PATH:指定命令的搜索路径

HOME:当前登录用户的家目录(即用户登陆到Linux系统中时,默认的目录)

PWD:当前工作路径

SHELL:查看命令行解释器shell是谁

HISTSIZE:可以查看用户之前输入的命令条数,上翻查看

(通过echo $环境变量名称查看哦)

我们可以通过env指令查看更多的环境变量:

如果我们想建立自己的环境变量,可以使用(export name=value):

export THIS_IS_MY_ENV=helloghs

可以这样取消创建的环境变量(unset name):

unset THIS_IS_MY_ENV

上面创建取消环境变量只会在内存级别上修改,并不会影响配置文件,因此重新登录即可恢复原状。

然后,我们还可以这样创建本地变量(name=value):

这样创建变量后,不能在env里查到,因此叫本地变量。

整体理解环境变量和系统

获取环境变量代码实现

在C语言中,存在一个环境变量environ,,这是一个二级指针:

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

int main()
{
  int i=0;
  extern char** environ;
  for(i=0;environ[i];i++)
  {
    printf("env[%d]->%s\n",i,environ[i]);
  }

}

通过以上代码可以获取环境变量,

我们惊奇地发现,上面打印出来的内容不正是刚才shell内部的环境变量吗?我们当前执行的程序./myprocess其实是bash的子进程,因此,环境变量默认也是可以被子进程拿到的!也就是说,环境变量刚开始不在子进程中,其实,环境变量们,默认在bash内部。

那么,我们如何理解上面那段打印环境变量的代码呢?

磁盘当中有系统级的配置文件,这些配置文件本身包含了环境变量,后来某一天登录了Linux系统,那么就要在内存里给系统分配一块内存,创建一个进程,这个进程就是bash(命令行解释器),然后将磁盘上的配置文件(包括环境变量)导入到了bash当中,然后再命令行中启动了我的程序(./myprocess),也就是在内存当中创建一个子进程,子进程可以拿到环境变量。

可是,环境变量可是很多的,bash内部是如何组织的?实际上,在系统启动时,会维护一张表,这张表是char* env[],这个数组中每个元素都是一个字符指针,指向一个字符串,这个字符串就是一个个环境变量,

这让我们联想到了main函数参数,bash进程启动的时候,默认会给子进程形成两张表,分别是argv[]命令行参数表和env[]环境变量表,bash通过各种方式交给子进程!agrv[]从用户输入的命令行来,env[]从OS的配置文件来!

所以,为了让用户快速找到环境变量表,所以系统提供了environ这个二级指针,这个指针是全局的,可以被子进程看到,指向char* env[]这张表,这张表的最后一个元素一定是NULL(所以上面代码循环会结束),因为这个表内部元素是char*类型,因此environ这个变量是char**。

那么,除了使用上面那段代码中environ这个全局变量获取环境变量表之外,还有一种方式,main函数除了有int agrc,char* argv[]之外,还可以接受第三个参数char* env[],通过这个参数打印环境变量:

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

int main(int argc,char* argv[],char* env[])
{
  int i=0;
  for(i=0;env[i];i++)
  {
    printf("env[%d]->%si\n",i,env[i]);
  }

  return 0;
}

环境变量再理解

环境变量具有系统级的全局属性,因为环境变量本身会被子进程继承下去,每个子进程都可以查到环境变量。

还可以使用getenv函数获取某个函数变量:

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
int main(int argc,char* argv[],char* env[])
{
  char* pth = getenv("PATH");
  if(pth == NULL) return 1;
  printf("path:%s\n",pth);

  return 0;
}

总结一下,共有3种方法获取环境变量:

1.extern char** environ;

2.通过main函数参数

3.getenv("PATH")

我们通过export导入一个环境变量:

export myval=11111

确实有这样一个变量,可是很奇怪的是,export不也是一个命令吗?不应该创建子进程吗?那创建的变量myval就不应该被父进程看到啊,可是上面看到确实导给了bash,事实上,export和echo命令都不是常规命令,是内建命令,80%的命令都是bash创建子进程执行的,但是,还有剩下20%的命令是直接由bash亲自执行的,

从这个例子中,可以看到,虽然把PATH改变了,但是部分命令仍可以用,这部分指令就是内建指令,由bash亲自执行。

我们再谈一个问题,在命令行当中,创建一个变量HELLO=666666,虽然可以用echo查到HELLO的存在,但是它不在环境变量表里,

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
int main(int argc,char* argv[],char* env[])
{
  char* pth = getenv("HELLO");
  if(pth == NULL) return 1;
  printf("path:%s\n",pth);

  return 0;
}

并且,运行这个程序,没有打印出任何内容,说明HELLO没有被这个子进程继承。像这种没有用export修饰过的,而直接创建的变量叫做本地变量。本地变量只可以echo、export这样的内建命令使用。

为了将其导入环境变量表,使用export HELLO将其导入,

同时,运行上面程序,发现HELLO这个变量已经可以被子进程获取。

综上,可以得出结论:本地变量只在bash内部有效,无法被子进程继承下去,导成环境变量,此时才能获取

进程地址空间

从代码看现象

我们先来从一段代码入手:

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

int main()
{
  int g_val = 100;
  printf("father is running,pid:%d,ppid:%d\n",getpid(),getppid());

  pid_t id = fork();
  if(id == 0)
  {
    //child
    int cnt = 0;
    while(1)
    {
      printf("I am child process,pid:%d,ppid:%d,g_val=%d,&g_val:%p\n",getpid(),getppid(),g_val,&g_val);
      sleep(1);
      cnt++;
      if(cnt == 5)
      {
        g_val = 300;
        printf("I am child process,change %d -> %d\n",100,300);
      }
    }
  }
  else
  {
    //father 
    while(1)
    {
      printf("I am father process,pid:%d,ppid:%d,g_val=%d,&g_val:%p\n",getpid(),getppid(),g_val,&g_val);
      sleep(1);
    }
  }

  return 0;
}

运行这段代码:

我们发现,子进程的g_val改变了,父进程的g_val并没有改变,这个可以理解,因为父子进程具有独立性。但是,最让我们感到震惊的是,红框里的地址竟然一样!怎么可能出现地址一样,但是内容不同呢?我们可以初步得出一个结论,这个地址绝对不是物理地址!这种地址在系统层面上将其称之为虚拟地址!

现在,我们尝试理解一下虚拟地址:

我们启动每一个进程时,除了要将代码和数据加载到内存之外,系统会为每一个进程创建一个叫做地址空间的东西,如下图:

在地址空间上的并不是实际地址,而是虚拟地址,还有一个叫做页表的东西将虚拟地址和物理地址之间建立映射关系,然后,未来上层使用虚拟地址访问时,OS会拿着虚拟地址查页表转化成物理地址,然后才能访问到数据。当创建子进程时,子进程也会有另外的对应的PCB、地址空间和页表,如果有100个子进程,那么会有100个地址空间,那么如何对这100个地址空间进行管理呢?先描述,再组织!地址空间本质上就是内核中的一个结构体对象, 子进程会把父进程的很多内核数据结构全拷贝一份,但是内存中的数据只有一份,父子进程的地址空间和页表一样,所以指向同一块内存,这种现象叫做浅拷贝。后来,子进程进行写入,通过地址空间找到虚拟地址,然后查找页表,找到实际地址,但是这个实际地址父进程也在用,所以会在物理内存中重新开辟一块空间,将旧的数据拷贝到新空间中,将新开辟空间的物理地址放到子进程页表中,重新构建映射,转而对应的目标指向新的空间,这个工作做完之后,继续进行刚才要进行的写入操作,至此,子进程写入操作完成,这些操作由OS自主完成,称为写时拷贝。父子进程页表中的虚拟地址相同,只是对应的实际地址不同。这就是为什么上面程序运行后,g_val的值不同,而地址相同。

地址空间细节理解

我们知道,进程之间是具有独立性的,如果父子进程是不写入的,那么未来一个全局变量,默认是被父子共享的,代码也是共享(只读的),为什么要这么干呢?可以可以把数据在创建子进程的时候,全部给子进程拷贝一份?这样貌似才符合独立性。我们想一想,很多数据可能子进程不会修改,这部分数据还可能占据很大空间,上来不管子进程改不改动数据,上来直接先拷贝一份数据,你能保证拷贝的数据都是要修改的吗?如果一些数据根本不会被修改,那直接共享就行了啊,再拷一份那不就是浪费内存资源了吗?所以,OS采用写时拷贝这种策略,本质是一种按需申请,这样就不会过度浪费空间。

那么,写时拷贝这种策略会不会比较慢呀?那如果按照所有数据都拷贝一份,那是不是也拷贝了?那写时拷贝这种策略要拷贝的数据量要≤全部拷贝一次的数据量。因此,写时拷贝通过调整拷贝的时间顺序,可以达到有效节省空间的目的。

如何理解地址空间

什么是划分区域

地址空间的本质是内核的一个struct结构体!内部很多的属性都是表示start,end的范围。

为什么要有地址空间

1)将无序变成有序,让进程以统一的视角看待物理内存以及自己运行的各个区域!

2)进程管理模块和内存管理模块解耦

3)拦截非法请求

如何理解虚拟地址

在最开始的时候,地址空间和页表里面的数据从哪里来呢?程序里面本身就有地址(虚拟地址)!所以,地址空间里的地址就是从程序里面读来的。

学习完上面的内容后,我们不难理解,为什么同一个id既可以等于0,又可以大于0。

Linux内核进程调度队列

Linux系统中每一个CPU都有一个运行队列,

上图是Linux2.6内核中进程队列的数据结构。

优先级

普通优先级: 100~ 139(我们都是普通的优先级,想想nice值的取值范围,可与之对应!)
实时优先级: 0~ 99(不关心)

活动队列

时间片还没有结束的所有进程都按照优先级放在该队列
nr_active: 总共有多少个运行状态的进程

queue[140]: 一个元素就是一个进程队列,相同优先级的进程按照FIFO规则进行排队调度,所以,数组下标就是优先级!

从该结构中,选择一个最合适的进程,过程是怎么的呢?

  1. 从0下表开始遍历queue[140]

  2. 找到第一个非空队列,该队列必定为优先级最高的队列

  3. 拿到选中队列的第一个进程,开始运行,调度完成!

  4. 遍历queue[140]时间复杂度是常数!但还是太低效了!

bitmap[5]:一共140个优先级,一共140个进程队列,为了提高查找非空队列的效率,就可以用5*32个比特位表示队列是否为空,这样,便可以大大提高查找效率!

过期队列

过期队列和活动队列结构一模一样

过期队列上放置的进程,都是时间片耗尽的进程

当活动队列上的进程都被处理完毕之后,对过期队列的进程进行时间片重新计算

active指针和expired指针

active指针永远指向活动队列
expired指针永远指向过期队列

可是活动队列上的进程会越来越少,过期队列上的进程会越来越多,因为进程时间片到期时一直都存在的。

没关系,在合适的时候,只要能够交换active指针和expired指针的内容,就相当于有具有了一批新的活动进程!

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

标签: linux 运维 服务器

本文转载自: https://blog.csdn.net/qq_48460693/article/details/136650699
版权归原作者 核动力C++选手 所有, 如有侵权,请联系我们删除。

“【Linux】进程”的评论:

还没有评论