Linux系统---进程概念


文章目录

  • 冯诺依曼体系结构
  • 操作系统(OS)
  • 进程的理解
  • 进程状态
  • 进程优先级
  • 环境变量
  • 进程地址空间
  • Linux2.6内核进程调度队列


一、冯诺依曼体系结构

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

  • 输入设备:包括键盘, 鼠标,扫描仪, 写板等
  • 存储器:就是内存
  • 输出设备:显示器,打印机等
  • 中央处理器(CPU):含有运算器和控制器和寄存器等

注意:

  • 磁盘等硬件不属于存储器。
  • 输入设备和输出设备统称为外设,所以磁盘等硬件都属于外设。
  • 不考虑缓存情况,这里的CPU能且只能对内存进行读写,不能访问外设(输入或输出设备)
  • 外设(输入或输出设备)要输入或者输出数据,也只能写入内存或者从内存中读取。
  • 所有设备都只能直接和内存打交道。
访问速度的不同:
         CPU>存储器>外设

当数据从外设中拿出时首先加载到存储器中,然后交给CPU处理,处理之后将结果返回给存储器,然后返回给外设去。这样以加快整体的速度,如果没有存储器,那么就是CPU与外设之间的交互,相比于没有加入存储器,会拖累整体的速度。

对冯诺依曼的理解,不能停留在概念上,要深入到对软件数据流理解上,请解释,从你登录上qq开始和某位朋友聊天开始,数据的流动过程。从你打开窗口,开始给他发消息,到他的到消息之后的数据流动过程。如果是在qq上发送文件呢?

首先从键盘中输入的你发给朋友的消息,此时外设有了数据,然后外设将数据加载到存储器中,CPU进行处理,然后将处理的结果返回给外设,,然后将数据发送给网卡,对方主机中的网卡接收到该消息后,将网卡上的数据加载到存储器中,然后让CPU处理,处理之后返回给存储器,最后将数据返回给电脑上的显示屏外设。

结论:

  •         冯诺依曼体系结构中CPU不会和外设直接进行交互访问,而是通过存储器的方式。
  •         冯诺依曼体系结构提高了整机的效率。

二、操作系统(OS)

概念:任何计算机系统都包含一个基本的程序集合,称为操作系统(OS)。笼统的理解,操作系统包括: 内核(进程管理,内存管理,文件管理,驱动管理) 其他程序(例如函数库,shell程序等等)
设计 OS 的目的 :与硬件交互,管理所有的软硬件资源;为用户程序(应用程序)提供一个良好的执行环境.
定位 :在整个计算机软硬件架构中,操作系统的定位是:一款纯正的“搞管理”的软件

操作系统:是一个管理软硬件间交互的软件。

管理的本质是在管理数据。操作系统通过使用结构体将这些硬件管理起来,结构体中存储的是关于这些硬件的各种属性信息。因为Linux是由C语言写的,所以采用struct 结构体来记录资源的属性,使用相关的数据结构和算法组织起来,再进行管理。

下图为计算机的软硬件体系结构示意图:

系统调用和库函数概念

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

总结:
计算机管理硬件
        1. 描述起来,用struct结构体
        2. 组织起来,用链表或其他高效的数据结构

三、进程的理解

1.基本概念:

概念 程序的一个执行实例,正在执行的程序等
内核角度 担当分配系统资源(CPU时间,内存)的实体。

进程=对应的PCB结构体+对应的代码和数据

2.进程-PCB结构体的理解

进程信息被放在一个叫做进程控制块的数据结构中,可以理解为进程属性的集合。
操作系统中称之为PCB(process control block),Linux操作系统下的PCB是: task_struc t
 
task_struct是Linux内核的一种数据结构,它会被装载到RAM(内存)里并且包含着进程的信息,该结构主要是用来记录该进程相关的属性。

3.task_ struct内容分类

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

4.组织进程和查看进程

组织进程:可以在内核源代码里找到它。所有运行在系统里的进程都以task_struct链表的形式存在内核里。

查看进程:进程的信息可以通过 /proc 系统文件夹查看

 如:要获取PID为1的进程信息,你需要查看 /proc/1 这个文件夹。

   大多数进程信息同样可以使用top和ps这些用户级工具来获取

单独使用ps aux显示出所有进程信息,利用grep命令来指定某个进程相关信息

 终止某个进程的方式可以使用ctrl+c,也可以使用kill -9 某个进程的pid来终止,ctrl+c本质上也是发送9号信号。

5.通过系统调用获取进程标示符

1.通过getpid和getppid的方式来获取父子进程表示符

进程id(PID)
父进程id(PPID)

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>

int main()
{
  printf("pid:%d\n",getpid());
  printf("ppid:%d\n",getppid());
  return 0;
}

 

2.利用fork来获取进程标示符

 fork是一个系统调用接口,其主要就是创建一个子进程,该函数执行一次,有两个返回值,父进程返回子进程的pid,,子进程返回0。其中父子进程代码共享,数据各自开辟空间,私有一份(采用写时拷贝)

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>

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

 

 由于fork之后,父子进程会共享代码发生了写时拷贝,由于fork之后,返回值不同,进而可以执行不同的代码块。fork之后,父子进程进行的先后顺序是由于cpu中的调度器和相应的调度算法决定。

fork函数,为什么有两次返回?相关链接

四、进程状态

1.Linux下的进程状态

  • R运行状态(running): 并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列里。
  • S睡眠状态(sleeping): 意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡眠 (interruptible sleep))。
  • D磁盘休眠状态(Disk sleep): 有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个状态的进程通常会等待IO的结束。
  • T停止状态(stopped) 可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行。
  • X死亡状态(dead): 这个状态只是一个返回状态,你不会在任务列表里看到这个状态。
  • Z(zombie)-僵尸状态 : 僵死状态(Zombies)是一个比较特殊的状态。当进程退出并且父进程 没有读取到子进程退出的返回代码时就会产生僵死(尸)进程僵死进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码。所以,只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程进入Z状态

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

2.进程阻塞和进程挂起

进程阻塞:正在运行的进程,由于等待某个事件发生而无法执行时,便放弃处理机而进入阻塞状态。引起进程阻塞的事件有很多种,例如,等待I/O完成、申请缓冲区不能满足、等待信号等。

进程挂起:当内存不足时,OS通过适当的置换进程的代码和数据到磁盘,进程的状态就叫做挂起。

3. 进程状态查看

ps aux / ps axj 命令
常见的进程状态查看命令:
  • ps aux | head -1 && ps aux | grep 进程PID
  • ps ajx |head -1 && ps ajx |grep 进程PID

  • 1)  ps a 显示现行终端机下的所有程序,包括其他用户的程序。
  • 2)ps -A 显示所有程序。
  • 3)ps c 列出程序时,显示每个程序真正的指令名称,而不包含路径,参数或常驻服务的标示。
  • 4)ps -e 此参数的效果和指定"A"参数相同。
  • 5)ps e 列出程序时,显示每个程序所使用的环境变量。
  • 6)ps f 用ASCII字符显示树状结构,表达程序间的相互关系。
  • 7)ps -H 显示树状结构,表示程序间的相互关系。
  • 8)ps -N 显示所有的程序,除了执行ps指令终端机下的程序之外。
  • 9)ps s 采用程序信号的格式显示程序状况。
  • 10)ps S 列出程序时,包括已中断的子程序资料。
  • 11)ps -t 指定终端机编号,并列出属于该终端机的程序的状况。
  • 12)ps u 以用户为主的格式来显示程序状况。
  • 13)ps x 显示所有程序,不以终端机来区分。
  • ps是显示当前状态处于running的进程,grep表示在这些里搜索,而ps aux是显示所有进程和其状态。

4.前后台进程

前台进程:

默认情况下,我们启动的每一个进程都是前台进程。它从键盘获得输入并发送它的输出到屏幕。

当一个进程运行在前台时,我们不能在同一命令行提示符下运行任何其他命令(启动任何其他进程),因为在程序结束它的进程之前命令行提示符不可用。

 从这里可以看出+表示该进程是一个前台进程。

后台进程:

利用kill -19 PID来暂停该进程

利用kill -18 PID来继续执行该进程

 暂停前:

 暂停时:

 继续执行:

 由上图我们可以知道该进程由前台进程变为后台进程,注意后台进程用ctrl+c无法停止,只能使用kill -9来终止该进程。

5.进程状态的分类

1.运行状态(R)

测试R状态代码:

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>

int main()
{
  int a=0;
  while(1){
    a=1+1;
  }
  return 0;
}

这里我们可以发现该进程状态为R+,表示该进程为前台进程就是正在运行或者是在运行队列中的进程。

 2.睡眠状态(S)

测试S状态代码:

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>

int main()
{
  printf("I am running\n");
  printf("我的pid是:%d\n",getpid());
  sleep(10);
  return 0;
}

 由上图我们可以知道该进程是处于S状态,表示该进程味着进程在等待事件完成。处于浅度睡眠状态可以随时被中断,也可以随时被终止,所以该状态可以叫做可中断睡眠状态。

3.磁盘休眠状态(D)

有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个状态的进程通常会等待IO的结束。

 

 所以该状态表示一个进程处以深度睡眠状态,该进程不会被操作系统释放,只有该进程自动唤醒时才可以恢复。

4.停止状态(T)

可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行

 测试T状态代码:

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>

int main()
{
 while(1){
  printf("我是一个进程pid:%d\n",getpid());
 }
return 0;
}

5.死亡状态(X)

这个状态只是一个返回状态,你不会在任务列表里看到这个状态。当父进程读取子进程的返回结果时,子进程立刻释放资源。死亡状态是非常短暂的,几乎不可能通过ps命令捕捉到。

6.僵尸状态(Z)

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

测试Z状态代码:

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

int main()
{
  printf("I am running...\n");
  pid_t id=fork();

  if(id==0){
    //child
    int cnt=5;
    while(cnt){
      printf("I am child,我的pid是:%d,我的ppid:%d,cnt:%d\n",getpid(),getppid(),--cnt);
      sleep(1);
    }
    printf("child quit\n");
    exit(1);
  }
  else{
    while(1){
      printf("I am father,我的pid是:%d,我的ppid:%d\n",getpid(),getppid());
      sleep(1);
    }
  }
  return 0;
}

 这里我们可以看到子进程变为Z+状态,该状态被称之为僵尸状态,该进程称之为僵尸进程。

 僵尸进程危害

  • 进程的退出状态必须被维持下去,因为他要告诉关心它的进程(父进程),你交给我的任务,我办的怎么样了。可父进程如果一直不读取,那子进程就一直处于Z状态。
  • 维护退出状态本身就是要用数据维护,也属于进程基本信息,所以保存在                task_struct(PCB)中,换句话说,Z状态一直不退出,PCB一直都要维护。
  • 那一个父进程创建了很多子进程,就是不回收,是不是就会造成内存资源的浪费。因为数据结构对象本身就要占用内存,想想C中定义一个结构体变量(对象),是要在内存的某个位置进行开辟空间!
  • 内存泄漏

7.孤儿状态

父进程如果提前退出,那么子进程后退出,进入Z之后,那该如何处理呢?

  • 父进程先退出,子进程就称之为“孤儿进程”
  • 孤儿进程被1号init进程领养,当然要有init进程回收喽。

测试孤儿状态代码:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
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;
}

 这里我们可以发现子进程被1号进程领养,该状态被称之为孤儿状态,该进程称为孤儿进程。

五、进程优先级

基本概念:

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

1.查看系统进程

在linux或者unix系统中,用ps –l命令则会类似输出以下几个内容:

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

2.PRI and NI

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

注意:进程的nice值不是进程的优先级,他们不是一个概念,但是进程nice值会影响到进程的优先级变化。可以理解nice值是进程优先级的修正修正数据。

3.查看进程优先级的命令

top 命令更改已存在进程的 nice:
top
进入top后按“r”–>输入进程PID–>输入nice值

4.关于进程的其他概念

  • 竞争性: 系统进程数目众多,而CPU资源只有少量,甚至1个,所以进程之间是具有竞争属性的。为了高效完成任务,更合理竞争相关资源,便具有了优先级
  • 独立性: 多进程运行,需要独享各种资源,多进程运行期间互不干扰
  • 并行: 多个进程在多个CPU下分别,同时进行运行,这称之为并行
  • 并发: 多个进程在一个CPU下采用进程切换的方式,在一段时间之内,让多个进程都得以推进,称之为并发

5.进程切换

进程切换是当今多任务多用户操作系统所应具有的基本功能。

操作系统为了控制进程的执行,必须有能力挂起正在CPU上运行的进程,并恢复以前挂起的某个进程的执行,这种行为被称为进程切换,任务切换或上下文切换。或者说,进行进程切换就是从正在运行的进程中收回处理器,然后再使待运行进程来占用处理器。 这里所说的从某个进程收回处理器,实质上就是把进程存放在处理器的寄存器中的中间数据找个地方存起来,从而把处理器的寄存器腾出来让其他进程使用。那么被中止运行进程的中间数据存在何处好呢?当然这个地方应该是进程的私有堆栈。

比如说:当进程1正在被执行的时候,此时CPU中的寄存器中的内容都是和进程1相关的,由于并发,CPU将从执行进程1变为执行进程2的时候,寄存器中原本属于进程1的内容就需要被保存,因为进程2同样会用到这些寄存器,就会覆盖掉原本的内容。目前可以认为,和进程1相关的寄存器中的内容,被复制到了PCB中。这一过程被叫做,进程切换中的上下文保护。当CPU再次从执行进程2变为执行进程1的时候,和进程2相关的寄存器中的内容同样会进行保护,并且将进程1的PCB中属于上下文保护的内容再恢复到CPU的寄存器中,覆盖掉属于进程2的内容,接着之前执行到的位置继续执行下去,这一位置由eip(PC指针)寄存器从恢复的数据中读取。这一过程被叫做,进程切换中的上下文恢复。

时间片:(timeslice)又称为“量子(quantum)”或“处理器片(processor slice)”是分时操作系统分配给每个正在运行的进程微观上的一段CPU时间(在抢占内核中是:从进程开始运行直到被抢占的时间)。假设这个时间是10ms。当进程1被执行了10ms,此时就会将进程1产生的临时数据进行上下文保护,也就是CPU寄存器中的内容都复制到对应的PCB中。再开始执行进程2,当进程2被执行了10ms后,同样会将进程2的上下文进行保护,然后将进程1的上下文进行恢复,继续执行进程1,如此往复,直到俩个进程结束。CPU中寄存器里的内容,只是属于当前被正在执行的进程。由于CPU执行的速度很快,一个进程被执行的时间很短,所以多个进程表现出来的结果就是一起被执行下去。而一个进程被执行的时长就是时间片。

六、环境变量

  • 环境变量(environment variables)一般是指在操作系统中用来指定操作系统运行环境的一些参数如:我们在编写C/C++代码的时候,在链接的时候,从来不知道我们的所链接的动态静态库在哪里,但是照样可以链接成功,生成可执行程序,原因就是有相关环境变量帮助编译器进行查找。
  • 环境变量通常具有某些特殊用途,还有在系统当中通常具有全局特性

1. 常见环境变量

  • PATH : 指定命令的搜索路径
  • HOME : 指定用户的主工作目录(即用户登陆到Linux系统中时,默认的目录)
  • SHELL : 当前Shell,它的值通常是/bin/bash。

2.查看环境变量方法

echo $PATH //PATH:你的环境变量名称
env | grep PATH

3.和环境变量相关的命令

  • 1. echo: 显示某个环境变量值
  • 2. export: 设置一个新的环境变量
  • 3. env: 显示所有环境变量
  • 4. unset: 清除环境变量
  • 5. set: 显示本地定义的shell变量和环境变量

4.环境变量的组织方式

每个程序都会收到一张环境表,环境表是一个字符指针数组,每个指针指向一个以’\0’结尾的环境字符串

5.通过代码如何获取环境变量

  打印命令行第三个参数

#include <stdio.h>
int main(int argc, char *argv[], char *env[])
{
    //我们给main函数传递的argc、argv[]参数,其实是传递的命令行中输入的程序名和选项!
    //char *env[]存储的是环境变量的地址
     int i = 0;
     for(; env[i]; i++)
     {
     	printf("%s\n", env[i]);
 	}
 	return 0;
}

 通过第三方变量environ获取

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

int main()
{
    //libc中定义的全局变量environ指向环境变量表,environ没有包含在任何头文件中,所以在使用时 要用extern声明
    extern char** environ;
    for(int i=0;environ[i];i++)
    {
        printf("%s\n",environ[i]);
    }
    return 0;
}

 通过系统调用获取或设置环境变量

#include <stdio.h>
#include <stdlib.h>
int main()
{
 printf("%s\n", getenv("PATH"));
 return 0;
}
常用getenv和putenv函数来访问特定的环境变量。

七、进程地址空间

进程地址空间布局图

测试地址空间代码:

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

int g_val=100;
int g_unval;

int main(int argc,char* argv[],char* envp[])
{
    printf("code addr:%p\n",main);
    char* str = "hello world";
    printf("read only addr:%p\n",str);
    printf("init addr:%p\n",&g_val);
    printf("uninit addr:%p\n",&g_unval);
    
    int* p = malloc(10);
    printf("heap addr:%p\n",p);
    
    printf("stack addr:%p\n",&str);
    printf("stack addr:%p\n",&p);
     
    int i=0;
    for(;i<argc;i++)
    {
        printf("args addr:%p\n",argv[i]);
    }

    i=0;
    while(envp[i])
    {
        printf("env addr:%p\n",envp[i]);
        i++;
    }
    return 0;   
}

 由上图可知,地址都是由低到高打印的。进而验证了地址空间分布情况。

1.深入理解进程地址空间

首先来看一段代码:

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

int g_val=100;

int main()
{
    pid_t id = fork();
    if(id == 0)
    {
        //child
        int flag = 0;
        while(1)
        {
            printf("我是子进程:%d, ppid: %d, g_val: %d, &g_val: %p\n\n", getpid(), getppid(), g_val, &g_val);
            sleep(1);
            flag++;
            if(flag == 5)
            {
                g_val=200;
                printf("我是子进程,全局数据我已经改了,用户你注意查看!\n");
            }
        }
    }
    else 
    {
        //parent
        while(1)
        {
            printf("我是父进程:%d, ppid: %d, g_val: %d, &g_val: %p\n\n", getpid(), getppid(), g_val, &g_val);
            sleep(2);
        }
    }
}

g_val都是指向同一个地址,为什么会打印两个不一样的值?

首先我们在全局变量没有改变之前我们打印的值和地址都是一样的。当子进程中g_val发生改变时,父进程中我们可以发现此时g_val并未发生改变。而此时地址都是一样的。根据进程间具有独立性,可以理解。而地址又是一样的,在同一个物理地址下中存在两个值,这是不太可能的,说明此时的地址不是真正的物理地址。

在Linux下这种地址叫做线性地址(虚拟地址),有时候也叫作逻辑地址,在语言层面上来说我们所说的地址都是虚拟地址。而物理地址基本上都是由OS系统进行管理。

2.进程地址空间区域的划分

" 用户虚拟地址空间 " 包括以下区域 :

① 代码段

② 数据段

③ 未初始化数据段

④ 动态库 代码段 , 数据段 , 未初始化数据段 ;

⑤ 堆内存 : 通过 malloc brk vmalloc 等函数 申请的 动态分配 的内存 ;

⑥ 栈内存 : 存放 局部变量 和 函数调用栈 ;

⑦ 内存映射区 : 将 文件 通过 mmap 函数 映射到 " 虚拟地址空间 " 的 " 内存映射区 " ;

⑧ 环境变量与参数 : 在 栈底 存放着程序运行的 环境变量 与 参数配置 信息 ;

在Linux操作系统中,每一个进程都独占4G。而进行调度时,由于CPU资源有限,因此只能通过时间片的方式在某个时刻来指定某个进程执行相应资源。

每个进程中都存在一个mm指针用来指向mm_struct结构体。该结构体将4GB的空间进行划分。

如下是Linux下的mm_struct底层源码:

struct mm_struct {
	struct vm_area_struct *mmap;		/* list of VMAs */
	struct rb_root mm_rb;
	u32 vmacache_seqnum;                   /* per-thread vmacache */
#ifdef CONFIG_MMU
	unsigned long (*get_unmapped_area) (struct file *filp,
				unsigned long addr, unsigned long len,
				unsigned long pgoff, unsigned long flags);
#endif
	unsigned long mmap_base;		/* base of mmap area */
	unsigned long mmap_legacy_base;         /* base of mmap area in bottom-up allocations */
#ifdef CONFIG_HAVE_ARCH_COMPAT_MMAP_BASES
	/* Base adresses for compatible mmap() */
	unsigned long mmap_compat_base;
	unsigned long mmap_compat_legacy_base;
#endif
	unsigned long task_size;		/* size of task vm space */
	unsigned long highest_vm_end;		/* highest vma end address */
	pgd_t * pgd;

	/**
	 * @mm_users: The number of users including userspace.
	 *
	 * Use mmget()/mmget_not_zero()/mmput() to modify. When this drops
	 * to 0 (i.e. when the task exits and there are no other temporary
	 * reference holders), we also release a reference on @mm_count
	 * (which may then free the &struct mm_struct if @mm_count also
	 * drops to 0).
	 */
	atomic_t mm_users;

	/**
	 * @mm_count: The number of references to &struct mm_struct
	 * (@mm_users count as 1).
	 *
	 * Use mmgrab()/mmdrop() to modify. When this drops to 0, the
	 * &struct mm_struct is freed.
	 */
	atomic_t mm_count;

	atomic_long_t nr_ptes;			/* PTE page table pages */
#if CONFIG_PGTABLE_LEVELS > 2
	atomic_long_t nr_pmds;			/* PMD page table pages */
#endif
	int map_count;				/* number of VMAs */

	spinlock_t page_table_lock;		/* Protects page tables and some counters */
	struct rw_semaphore mmap_sem;

	struct list_head mmlist;		/* List of maybe swapped mm's.	These are globally strung
						 * together off init_mm.mmlist, and are protected
						 * by mmlist_lock
						 */


	unsigned long hiwater_rss;	/* High-watermark of RSS usage */
	unsigned long hiwater_vm;	/* High-water virtual memory usage */

	unsigned long total_vm;		/* Total pages mapped */
	unsigned long locked_vm;	/* Pages that have PG_mlocked set */
	unsigned long pinned_vm;	/* Refcount permanently increased */
	unsigned long data_vm;		/* VM_WRITE & ~VM_SHARED & ~VM_STACK */
	unsigned long exec_vm;		/* VM_EXEC & ~VM_WRITE & ~VM_STACK */
	unsigned long stack_vm;		/* VM_STACK */
	unsigned long def_flags;
	unsigned long start_code, end_code, start_data, end_data;
	unsigned long start_brk, brk, start_stack;
	unsigned long arg_start, arg_end, env_start, env_end;

	unsigned long saved_auxv[AT_VECTOR_SIZE]; /* for /proc/PID/auxv */

	/*
	 * Special counters, in some configurations protected by the
	 * page_table_lock, in other configurations by being atomic.
	 */
	struct mm_rss_stat rss_stat;

	struct linux_binfmt *binfmt;

	cpumask_var_t cpu_vm_mask_var;

	/* Architecture-specific MM context */
	mm_context_t context;

	unsigned long flags; /* Must use atomic bitops to access the bits */

	struct core_state *core_state; /* coredumping support */
#ifdef CONFIG_AIO
	spinlock_t			ioctx_lock;
	struct kioctx_table __rcu	*ioctx_table;
#endif
#ifdef CONFIG_MEMCG
	/*
	 * "owner" points to a task that is regarded as the canonical
	 * user/owner of this mm. All of the following must be true in
	 * order for it to be changed:
	 *
	 * current == mm->owner
	 * current->mm != mm
	 * new_owner->mm == mm
	 * new_owner->alloc_lock is held
	 */
	struct task_struct __rcu *owner;
#endif
	struct user_namespace *user_ns;

	/* store ref to file /proc/<pid>/exe symlink points to */
	struct file __rcu *exe_file;
#ifdef CONFIG_MMU_NOTIFIER
	struct mmu_notifier_mm *mmu_notifier_mm;
#endif
#if defined(CONFIG_TRANSPARENT_HUGEPAGE) && !USE_SPLIT_PMD_PTLOCKS
	pgtable_t pmd_huge_pte; /* protected by page_table_lock */
#endif
#ifdef CONFIG_CPUMASK_OFFSTACK
	struct cpumask cpumask_allocation;
#endif
#ifdef CONFIG_NUMA_BALANCING
	/*
	 * numa_next_scan is the next time that the PTEs will be marked
	 * pte_numa. NUMA hinting faults will gather statistics and migrate
	 * pages to new nodes if necessary.
	 */
	unsigned long numa_next_scan;

	/* Restart point for scanning and setting pte_numa */
	unsigned long numa_scan_offset;

	/* numa_scan_seq prevents two threads setting pte_numa */
	int numa_scan_seq;
#endif
#if defined(CONFIG_NUMA_BALANCING) || defined(CONFIG_COMPACTION)
	/*
	 * An operation with batched TLB flushing is going on. Anything that
	 * can move process memory needs to flush the TLB when moving a
	 * PROT_NONE or PROT_NUMA mapped page.
	 */
	bool tlb_flush_pending;
#endif
	struct uprobes_state uprobes_state;
#ifdef CONFIG_HUGETLB_PAGE
	atomic_long_t hugetlb_usage;
#endif
	struct work_struct async_put_work;
};

3.进程地址空间与物理内存之间的联系

当磁盘中的某个可执行程序文件,经历了预处理,编译,汇编,链接等四个过程,在这个过程中编译器按照进程地址空间的分布,将形成后的文件的地址被称为逻辑地址,也叫作虚拟地址。这样该文件从磁盘中加载到内存上,内存将分配空间给它,存放代码,而代码中的地址却是虚拟地址。然后OS通过一个页表将虚拟地址与物理地址进行映射。

这样我们可以回答上面的g_val值不同的情况了?

  • 在父子进程的进程地址空间中,相同的地址处都有一个全局变量g_val,通过各自的页表,映射在了物理内存中。
  • 在这个全局变量每一发生改变的时候,父子进程中该值是一样的,所以在物理内存中俩个进程只需要映射到一块物理空间即可。当子进程改变了它的全局变量,此时由于俩个进程该变量的值不一样了,所以不能再映射到一块物理空间中了。所以此时操作系统会将原本在物理空间中的全局变量拷贝一份,放在另一块空间中,并且将新物理空间的物理地址更新到子进程的页表中。此时父子进程中,全局变量在各自的进程地址空间中的虚拟地址仍然是相同的,但是各自页表中对应的物理地址已经不同了。
  • 在上诉过程中,当子进程将全局变量改变以后,操作系统将原本物理空间中的值拷贝一份放到新的空间中,这一行为叫做写时拷贝。

4.进程地址空间存在的意义

  • 1.凡是非法的访问或者是映射OS都是识别出来,并终止该进程,进而有效的保护了物理内存。因为地址空间和页表都是OS创建出来并且维护的,所以凡是想使用地址空间和页表进行映射的,一定是在OS的监管之下进行访问的。这样保护了物理内存中的所有合法数据,也保护了所有进程,以及内核的相关有效数据。
  • 2.由于存在地址空间和页表映射的存在,磁盘中的数据可以在物理内存中任意加载。由于内存管理模块和进程管理模块完成了解耦合,进而保证了进程的独立性。由于地址空间的存在,所以上层申请空间时,其实是在地址空间中申请的,OS采用延迟分配的策略,来提高整机的效率。而当你真正进行物理地址空间访问时,才执行相应的管理算法,帮你申请内存,构建页表映射关系。
  • 3.由与物理内存在理论上可以任意加载,正是因为页表的存在,可以将地址空间中的虚拟地址通过也表和物理地址进行映射。正是由于地址空间的存在,每一个进程都认为自己拥有4GB空间,而且每个区域都是有序的,每个进程通过页表来映射到不同区域进而实现进程的独立性。

八、Linux2.6内核进程调度队列

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

 一个CPU拥有一个runqueue

如果有多个CPU就要考虑进程个数的负载均衡问题
优先级
  • 普通优先级: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指针的内容,就相当于有具有了一批新的活动进程!

猜你喜欢

转载自blog.csdn.net/qq_67458830/article/details/131883104