初相识进程

目录

进程概念

冯诺依曼体系结构

操作系统(OS)

概念

设计操作系统的目的

操作系统的管理

系统调用和库的关系

进程

描述进程——PCB

内容分类

查看进程

通过系统调用获得进程标示符

进程状态

Linux内核中关于进程状态的源代码

R(运行状态)

S(浅度睡眠状态)

D(深度睡眠状态)

T(停止状态)

X(死亡状态)

进程状态查看

僵尸进程

 僵尸进程危害

孤儿进程

进程优先级

基本概念

查看系统进程

优先级的计算

调整进程优先级的命令

进程竞争/独立/并行/并发的概念

进程切换

 Linux2.6内核进程O(1)调度队列

一个CPU拥有一个runqueue

优先级

活动队列

过期队列

 active指针和expired指针

命令行参数

环境变量

 基本概念

常见环境变量

和环境变量相关的命令

查看环境变量方法

测试PATH

测试HOME

环境变量的组织方式

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

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

环境变量通常是具有全局属性的

程序地址空间

虚拟地址

进程地址空间

虚拟内存管理

 为什么要有虚拟地址空间


进程概念

冯诺依曼体系结构

下面我们详细说一下图中的设备:

存储器:其实就是内存(外部设备和CPU的缓存)

CPU-运算器:算数运算,逻辑运算

CPU-控制器:执行代码,进行逻辑控制

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

输出设备:显示器,喇叭(音响),磁盘,网卡,打印机...

注:

1.磁盘和硬盘的区别:“磁盘”专指老式的磁性盘片技术;硬盘是更大概念,既包括磁盘(HDD),也包括没盘片的固态(SSD)。

2.CPU在数据层面,不与外设打交道,只与内存打交道,所以C/C++的可执行程序必须加载到内存,CPU才能从内存中提取代码,在控制器中执行代码。

3.数据流动的本质是拷贝,所以计算机的整体效率,本质就是设备间拷贝的效率。

4.效率:外部设备<<内存<<CPU

5.liunx下,一切皆文件,故打开文件,本质就是打开有关输入设备的文件。

6.从网络中获取数据的本质就是通过网络,数据被拷贝到输入设备——网卡中。网卡就是让你的电脑能“上网”的硬件,是电脑和网络之间的翻译官和邮差。

根据上面学习的内容,描述一下两个人之间互发信息,数据流动怎么走?

操作系统(OS)

概念

是一款管理软硬件资源的软件。笼统的理解,操作系统包括:内核(进程管理,内存管理,文件管理,驱动管理),其他程序(例如函数库,shell程序等等)

设计操作系统的目的

对下,与硬件交互,管理所有的软硬件资源
对上,为用户程序(应用程序)提供一个良好的执行环境

下面是一张计算机系统结构分层示意图:

在图中我们发现,

1.硬件是由软件部分的驱动程序和操作系统管理的,所以在日常生活中,我们使用计算机,其实是通过软件去访问计算机的硬件资源。但硬件资源是有限的,调用的时候可能会出现供不应求的状态,所以必须把硬件资源管理好。

2.软硬件体系结构本质是层状的,所以任何计算机问题,都可以通过添加一层软件层来解决。

那操作系统是怎么管理的呢?

操作系统的管理

如果用学校的职位来比喻的话,那么相当于校长是操作系统,辅导员是驱动程序,而学生就是硬件。因为在学校,校长属于是管理者——发布命令和要求,辅导员是决策的执行者——执行命令和要求,管理学生做好命令与要求里的事情。那校长怎么管理学生呢?先描述在组织。校长就可以对学生信息进行增删查改了。

所以管理的本质是对数据进行管理,管理的做法是先描述,在组织

系统调用和库的关系

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

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

还没学习进程之前,大家知道操作系统是怎么进行进程管理的呢?结合上面重要的知识点:先描述在组织。先描述进程,再把进程组织起来。

进程

定义:程序的一个执行实例,正在执行的程序等。

从内核的角度:担当分配系统资源的实体。

进程=内核数据结构(task_struct)+自己程序代码和数据。

当我们在windows系统下按下ctrl+shift+Esc键的时候,我们进入任务管理器的进程页面。

上面这些进程就是我们电脑面前进行的执行程序。

描述进程——PCB

在Linux系统下,PCB(进程控制块的数据结构体)就是tast_struct。PCB中存放进程信息——进程属性的集合,它会被装载进RAM(内存)里并且包含着进程的信息。

进程排队,本质上是pcb节点进行排队。实际上就是从一个数据结构(struct PCB)中,把节点拿走,放入新的队列数据结构(struct runqueue)中。后面讲解进程就绪状态时会讲到。总的来说,就是哪一个pcb节点被调用,就说明该进程被调用。

内容分类

上面图片中PCB结构体描述没那么准确,下面的才是PCB的详细部分描述。

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

获取进程标示符的函数:getpid()后面详讲。

使用方式:例:pid id=getpid();

查看进程

1.通过/proc系统文件夹查看

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

//test.c
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
    while(1)
    {
        sleep(1);
    }
    return 0;
}

ps命令用来查看当前进程的进程信息,grep命令是用来查找当前目录中相同字符。比如grep test查找test文件,grep -v grep用来反向搜索。

[root@localhost d1]# ps aux | grep test |grep -v grep
root       3576  0.0  0.0   4212   352 pts/1    S+   20:15   0:00 ./test
[root@localhost d1]# ps aux | grep test 
root       3576  0.0  0.0   4212   352 pts/1    S+   20:15   0:00 ./test
root       3615  0.0  0.0 112824   988 pts/0    S+   20:19   0:00 grep --color=auto test
//grep -v grep 反向搜索没有grep的进程。

所以ps aux | grep test |grep -v grep这条命令实际上是:ps aux把所有当前进程的进程信息都放在管道文件中,然后grep从中找到有test关键字的文件,但是里面还有一条grep命令留下的痕迹,所以grep -v grep用来剔除grep命令留下的痕迹。

通过系统调用获得进程标示符

进程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;
}
//结果:
[root@localhost d1]# gcc -o test test.c
[root@localhost d1]# ./test
pid:3968
ppid:3485
[root@localhost d1]# ./test
pid:3998
ppid:3485

但是在上面的结果中我们发现,明明运行的是同一个程序,进程的代码在变化,而父进程的代码一直不变,这是怎么回事?其实在我们命令行中,启动命令/程序的时候,都会变成进程,这个进程就是父进程——bash(外壳shell)。那怎么证明父进程是bash呢?

[root@localhost d1]# ps ajx | head -1 && ps axj |grep 3485
  PPID    PID   PGID    SID TTY       TPGID STAT   UID   TIME COMMAND
  3409   3412   3412   3412 pts/1      3485 Ss    1000   0:00 -bash
  3412   3476   3476   3412 pts/1      3485 S        0   0:00 su
  3476   3485   3485   3412 pts/1      3485 S+       0   0:00 bash
  3089   4215   4214   2683 pts/0      4214 S+       0   0:00 grep --color=auto 3485

Linux系统中,父进程通过创建子进程的方式可以增多进程,而且父进程(bash)会让子进程来执行代码。

那bash如何创建子进程呢?

函数:fork()

返回值:

1.man fork查看fork函数

2./+字符,在man页面查找字符

在图中我们发现,fork成功后,fork有两个返回值:父进程返回的是子进程的PID,子进程成功返回0;fork失败,父进程返回-1。

特点:

1.fork后有两个返回值。因为有两个进程正在执行。

2.父子进程代码共享,数据各自开辟空间,私有一份(才有写时拷贝)。因为进程间具有独立性。

   #include<stdio.h>
   #include<sys/types.h>
   #include<unistd.h>
   int main()
   {
       printf("我是一个进程:pid:%d,ppid:%d\n",getpid(),getppid());
       fork();
       printf("fork后:pid:%d,ppid:%d\n",getpid(),getppid());
       return 0;
  }
//结果:
[root@localhost d1]# ./test
我是一个进程:pid:4584,ppid:3485
fork后:pid:4584,ppid:3485
fork后:pid:4585,ppid:4584

再上面的运行结果中,我们发现fork后面的那条语句打印了两遍,但是两遍的结果不一样,这是为什么?但是我们发现第一条“fork后:pid:4405,ppid:3485”和“我是一个进程:pid:4405,ppid:3485”中pid和ppid的值是一样的,说明第一条是父进程打印的,第二条是子进程打印的。

如果我们对代码中的一些变量进行更改,是不是父进程和子进程还是一样呢?

   #include<stdio.h>
   #include<sys/types.h>
   #include<unistd.h>
   int main()
   {
       printf("我是一个进程:pid:%d,ppid:%d\n",getpid(),getppid());
       int ret;
       pid_t id=fork();
       if(id==0)//子进程
      {
          ret=10;
      }
      else if(id>0)//父进程
      {
          ret=30;
      }
      printf("fork后:pid:%d,ppid:%d,ret=%d\n",getpid(),getppid(),ret);
      return 0;
  }
//结果:
[root@localhost d1]# ./test
我是一个进程:pid:4760,ppid:3485
fork后:pid:4760,ppid:3485,ret=30//父进程的ret数据是30
fork后:pid:4761,ppid:4760,ret=10//子进程的ret数据是10
//一条代码,父子进程的数据不同

上面这位代码我们也发现了子进程的运行代码和父进程的运行代码一样,说明了父子进程共用同一份代码,但是上面我们发现父子进程数据更改,同一个变量父进程和子进程不一样,说明了父子进程的数据是私有的,是单独开辟了空间。

我们如果父子进程都打印同一条代码,这样就显得fork没有什么意义。根据fork返回值的不同,我们可以分清父子进程,那么我们就可以通过if分流的方式,让父子进程执行不同的任务。像上面的代码一样。

进程状态

“进程状态”这个词听起来像是要讲技术,但咱们今天不打算一上来就甩一堆术语。我想先请你想象一个场景:

你正在机场候机,大厅屏幕上的航班状态不断跳动——“已起飞”“登机中”“延误”“取消”。每一个状态背后,都是一架飞机、一组机组、几百名乘客的下一步命运。

操作系统里的进程也一样。它们不是冷冰冰的“任务”,而是一场场正在发生的旅程:有的刚刚拿到登机牌(新建),有的正在跑道加速(运行),有的被空管临时要求盘旋等待(阻塞),还有的已经降落却还在等廊桥(僵尸)。今天,我们就当一次“塔台管制员”,一起看懂这些“航班”——也就是进程——到底在哪、在干什么、下一步会去哪儿。

Linux内核中关于进程状态的源代码

为了弄明白正在运行的进程是什么意思,我们需要知道进程的不同状态。一个进程可以有几个状
态(在Linux内核⾥,进程有时候也叫做任务)。
下面的状态在kernel源代码里定义:

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

R(运行状态)

并不意味着进程一定在运行中,它表明进程要么在运行中要么在运行队列里。如果进程的PCB处在CPU的调度队列中,进程就在运行状态,都可以随时等待CPU调度执行。(后面结合优先级和调度队列一起讲)

S(浅度睡眠状态)

意味着进程在等待时间完成(有时候也叫可中断睡眠状态)。简单来说就是在等待事件的到来,比如在程序中需要硬件,但是硬件一直都没有响应,那么该进程就进入浅度睡眠状态,如果硬件响应了,那么该进程就被激活回到运行状态。

  #include<stdio.h>
  int main()
  {
     printf("I am sleeping...\n");
     sleep(100);//当作在等待某个事件的响应
     return 0;
  }

D(深度睡眠状态)

有时候也叫不可中断睡眠状态,在这个状态的进程通常会等待I/O的结束。处在D状态的进程是怎么样都杀不死的,只有自己“自愿”苏醒才行。

当进程向块设备(磁盘、MD、swap区...)提交I/O请求(内存和磁盘之间传输数据都要发I/O请求),即把任务交给设备后原地等中断通知在I/O请求完成之前,当前进程都不能做任何事情,也不能被信号打断。

T(停止状态)

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

[root@localhost d1]# ps aux| grep testStat |grep -v grep
root       8070  0.0  0.0   4216   352 pts/1    S+   22:08   0:00 ./testStat
[root@localhost d1]# kill -SIGSTOP 8070
[root@localhost d1]# ps aux| grep testStat |grep -v grep
root       8070  0.0  0.0   4216   352 pts/1    T    22:08   0:00 ./testStat
[root@localhost d1]# kill -SIGCONT 8070
[root@localhost d1]# ps aux| grep testStat |grep -v grep
root       8070  0.0  0.0   4216   352 pts/1    S    22:08   0:00 ./testStat

X(死亡状态)

这个进程只是一个返回状态,当进程退出信息被读取后,该进程申请的资源立即被释放,进程PID被内核回收,所以在任务列表中看不到该状态。

注:阻塞状态有两种:S(浅度睡眠状态)和D(深度睡眠状态)

进程状态查看

ps aux / ps axj 命令

• a:显示⼀个终端所有的进程,包括其他用户的进程。
• x:显示没有控制终端的进程,例如后台运行的守护进程。
• j:显示进程归属的进程组ID、会话ID、父进程ID,以及与作业控制相关的信息
• u:以用户为中心的格式显示进程信息,提供进程的详细信息,如用户、CPU和内存使用情况等

:进程状态是保存在进程数据块的结构体中,也就是task_struct中。

僵尸进程

• 僵死状态(Zombies)是⼀个比较特殊的状态。当进程退出(代码数据都被释放,但是会把task_struct保留)并且父进程没有读取到子进程退出的返回代码时就会产生僵死(尸)进程
• 僵死进程会以终止状态保持在进程表中,并且会⼀直在等待父进程读取退出状态代码。 所以,只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程进入Z状态。

来⼀个创建维持30秒的僵死进程例子:

   #include<stdio.h>
   #include<unistd.h>
   #include<sys/types.h>
   #include<stdlib.h>
   int main()
   {
       pid_t id=fork();
       if(id==0)
       {
          //子进程
          printf("子进程[%d]is begin Z...\n",getpid());
          sleep(5);
          exit(EXIT_SUCCESS);//子进程五秒后退出
      }
      else if(id<0)
      {
          perror("fork");
          return 1;
      }
      else
     {
         //父进程
          printf("父进程[%d]is sleeping...\n",getpid());
          sleep(30);//父进程三十秒后退出,子进程比父进程先退出
      }
      return 0;
  }

监控变化命令:

 while :;do ps aux| grep testStat |grep -v grep ;sleep 1;echo "################";done

//解析:是一个死循环,如果testStat不运行的话,隔一秒打印一下分隔符(“#”),testStat运行的时候,第六秒的时候子进程由睡眠状态变成僵尸状态。如下图所示。

 僵尸进程危害

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

如果父进程提前退出,那子进程又会是什么进程?那该如何处理呢?

孤儿进程

父进程先退出,子进程就称之为“孤儿进程”
孤儿进程被1号init/systemd进程领养,最后有init/systemd进程回收

   #include<stdio.h>
   #include<unistd.h>
   #include<sys/types.h>
   #include<stdlib.h>
   int main()
   {
       pid_t id=fork();
       if(id==0)
       {
          //子进程
          printf("子进程[%d]is sleeping...\n",getpid());
          sleep(30);
          
      }
      else if(id<0)
      {
          perror("fork");
          return 1;
      }
      else
      {
         //父进程
          printf("父进程[%d]\n",getpid());
         sleep(5);
          exit(EXIT_SUCCESS);
    }
  
      return 0;
  }

进程优先级

基本概念

cpu资源分配的先后顺序,就是进程的优先权。因为进程有很多,但是资源(cpu,硬件等)少。

优先权高的进程有优先执行权利。配置进程优先权对多任务环境的Linux很有用,可以改善系统性能。还可以把进程运行到指定的CPU上,这样一来,把不重要的进程安排到某个CPU,可以大大改善系统整体性能。

查看系统进程

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

我们很容易注意到其中的几个重要信息,有下:

UID:代表执行者的身份

PID:代表这个进程的代号(标识符)

PPID:代表这个进程的父进程的代号(标示符)

PRI:代表这个进程可以被执行的优先级,其值越小越早被执行

NI:代表这个进程的nice值,进程可被执行的优先级的修正数值。

优先级的计算

怎么计算优先级(PRI)呢?

PRI(new)=PRI(old)+nice

PRI优先级是由初始值的,通过ps -al就可以知道,初始值为80。

PRI值越小越快被执行,当nice值为负值的时候,那么该程序的优先级将变小,所以调整进程优先级,在Linux下就是调整nice值。因为Linux是分时操作系统,尽可能要公平公正,所以优先级要变,也要在可控范围内变化。所以nice取值是有范围的:-20至19,一共四十个级别。

调整进程优先级的命令

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

进入top后按“r”->输入进程PID->enter->输入修改变化后nice值

我们发现nice修改为20的时候变成的是19,因为nice的值最高只能是19,超过了19的值也只能被改成19。

注:

1.其他调整优先级命令:nice,renice

2.系统函数也可以改变nice值:

#include <sys/time.h>
#include <sys/resource.h>
int getpriority(int which, int who);
int setpriority(int which, int who, int prio);

进程竞争/独立/并行/并发的概念

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

独立性:多进程运行,需要独享各种资源,多进程运行期间互不干扰。简单来说就是进程与进程间是互相隔离的。(重点)

并行性:多个进程在多个CPU下分别同时运行。

并发性:多个进程在一个CPU下采用进程切换(即任意时刻都只有一个进程在运行)的方式,在一段时间内,让多个进程都得以推进,称之为并发。

进程切换

背景:

1.CPU内,寄存器就是一套存储空间,寄存器只有一套。但是,寄存器内部的数据可以有很多份。

2.CPU上下文切换:其实际含义是任务切换, 或者CPU寄存器切换。当多任务内核决定运行另外的任务时, 它保存正在运行任务的当前状态, 也就是CPU寄存器中的全部内容。这些内容被保存在任务自己的堆栈中, 入栈工作完成后就把下⼀个将要运行的任务的当前状况从该任务的栈中重新装入CPU寄存器,并开始下一个任务的运行, 这一过程就是context switch。

那什么时候进程A会切换到进程B呢?

我们当前的系统(Linux、windows等)都是分时操作系统,每一个进程都有自己的时间片。时间片其实就是一个计数器,时间片的时间完结了,进程就被操作系统从CPU中切换出来。

那么被操作系统切换出来的进程又被放在哪里呢?我们都知道内存中有一个调度队列(runqueue),它是怎么分配进程的呢?

 Linux2.6内核进程O(1)调度队列

一个CPU拥有一个runqueue

如果有多个CPU就要考虑进程个数的负载均衡问题。

优先级

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

活动队列

时间片还没有结束的所有进程都按照优先级放在该队列
• nr_active: 总共有多少个运行状态的进程
• queue[140]: 一个元素就是一个进程队列,相同优先级的进程按照FIFO规则进行排队调度,所以,
数组下标就是优先级!FIFO规则其实就是谁先来谁先排前面
• bitmap[5]:一共140个优先级,一共140个进程队列(一个优先级中包含的进程放在一个队列中),为了提高查找非空队列的效率,就可以用5*32个比特位表示队列是否为空,这样,便可以大大提高查找效率!

• 从该结构中,选择一个最合适的进程,过程是怎么的呢?
1. 从0下表开始遍历queue[140]
2. 找到第一个非空队列,该队列必定为优先级最高的队列
3. 拿到选中队列的第一个进程,开始运行,调度完成!
4. 遍历queue[140]时间复杂度是常数!但还是太低效了!所以应用了bitmap数组,用位图的方式记录queue中哪个进程队列空缺。

过期队列

• 过期队列和活动队列结构一模一样
• 过期队列上放置的进程,都是时间片耗尽的进程
• 当活动队列上的进程被处理完毕之后,对过期队列的进程进行时间片重新计算

 active指针和expired指针

• active指针永远指向活动队列
• expired指针永远指向过期队列
• 可是活动队列上的进程会越来越少,过期队列上的进程会越来越多,因为进程时间片到期前,全部进程一直都存在。
• 但是,在合适的时候,只要交换active指针和expired指针的内容,就相当于有具有了⼀批
新的活动进程!

命令行参数

我们经常使用main函数的时候,都没有给main函数中写参数,但是main函数真的没有参数吗?main函数其实有三个参数。

但是最标准、最通用的写法只有两个参数:

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

//argc:统计argv数组中元素的个数

//argv数组:指向一个一个字符串的指针数组,这个指针数组指向的是命令行

   #include<stdio.h>
   int main(int argc,char*argv[])
   {
       int i=0;
       for(i=0;i<argc;i++)
       {
           printf("argv[%d]:%s\n",i,argv[i]);
       }
       return 0;
  }

运行结果:

[root@localhost d1]# ./testmain -v1 -v2 -v3 -v4//空格隔开表示一个子串
argv[0]:./testmain
argv[1]:-v1
argv[2]:-v2
argv[3]:-v3
argv[4]:-v4
[root@localhost d1]# ./testmain
argv[0]:./testmain

其实命令行本身是一个程序,由bash来解释这些命令行,运行程序。比如ls -l -a命令行,本质是字符串,可以通过一定的方式传给ls内部的“main”,在ls内部实现的时候,就可以根据不同的选项,实现类似功能的不同表达。

环境变量

 基本概念

 • 环境变量(environment variables)一般是指在操作系统中用来指定操作系统运行环境的⼀些参数。

如:我们在编写C/C++代码的时候,在链接的时候,从来不知道我们的所链接的动态静态库在哪
里,但是照样可以链接成功,生成可执行程序,原因就是有相关环境变量帮助编译器进行查找。

• 环境变量通常具有某些特殊用途,还有在系统当中通常具有全局特性

常见环境变量

• PATH : 指定命令的搜索路径
• HOME : 指定用户的主工作目录(即用户登陆到Linux系统中时,默认的目录)
• SHELL : 当前Shell,它的值通常是/bin/bash,也可以设置成/bin/zsh等,即系统告知用了哪个shell程序(解释命令、运行别的进程)

和环境变量相关的命令

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

[root@localhost d1] unset MY_HELLO

[root@localhost d1] set

查看环境变量方法

echo $NAME //NAME:你的环境变量名称

举例:

[root@localhost d1]# echo $PATH
/usr/local/bin:/usr/bin:/usr/local/sbin:/usr/sbin
[root@localhost d1]# echo $SHELL
/bin/bash
[root@localhost d1]# echo $HOME
/root

测试PATH

1. 创建hello.c文件

#include <stdio.h>
int main()
{
    printf("hello world!\n");
    return 0;
}

2. 对比 ./hello 执行和之间 hello 执行

[root@localhost d1]# gcc -o hello hello.c
[root@localhost d1]# hello
bash: hello: 未找到命令...
[root@localhost d1]# ./hello
hello world!

[root@localhost d1]# /home/d1/hello
hello world!

3. 为什么有些指令可以直接执行,不需要带路径,而我们的二进制程序需要带路径才能执行?

因为二进制程序不在$PATH环境变量中,不是bash的内置命令(bash里面已经有的命令,如:cd、pwd、echo等)。操作系统查找可执行命令,是在环境变量PATH中查找的。

4. 将我们的程序所在路径加入环境变量PATH当中, export PATH=$PATH:hello程序所在路径

[root@localhost d1]# export PATH=$PATH:hello
[root@localhost d1]# hello
hello world!

测试HOME

1. 用root和普通用户,分别执行 echo $HOME ,对比差异

[root@localhost d1]# echo $HOME
/root
[root@localhost d1]# su slm
[slm@localhost d1]$ echo $HOME
/home/slm

2. 执行 cd ~后执行pwd ,对应 ~ 和 HOME 的关系

[root@localhost slm]# cd ~
[root@localhost ~]# pwd
/root

[slm@localhost d1]$ cd ~
[slm@localhost ~]$ pwd
/home/slm

//~的地址就是当前用户的家目录,正是$HOME的值

环境变量的组织方式

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

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

方法一:main的第三个参数

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

#include <stdio.h>
int main(int argc, char *argv[], char *envp[])
{
    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;
}

libc中定义的全局变量environ指向环境变量表,environ没有包含在任何头文件中,所以在使用时要用
extern声明。

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

命令:getenv 

#include <stdio.h>
#include <stdlib.h>
int main()
{
    printf("%s\n", getenv("PATH"));
    return 0;
}

常用getenv函数来访问特定的环境变量。

环境变量通常是具有全局属性的

环境变量通常具有全局属性,可以被子进程继承下去

#include <stdio.h>
#include <stdlib.h>
int main()
{
    char *env = getenv("MYENV");
    if(env)
    {
        printf("%s\n", env);
    }
    return 0;
}

直接查看,发现没有结果,说明该环境变量根本不存在
• 导出环境变量
export MYENV="hello world"
• 再次运行程序,发现结果有了!说明:环境变量是可以被子进程继承下去的

程序地址空间

我们在讲C语言的时候,画过这样的空间布局图,这是在32位机器下,虚拟地址空间的范围为【0,2^32],即【2,4G】。64位机器下的虚拟地址空间范围大小比32位大。

虚拟地址

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int g_val = 0;
int main()
{
pid_t id = fork();
if(id < 0){
perror("fork");
return 0;
}
else if(id == 0){ //child
printf("child[%d]: %d : %p\n", getpid(), g_val, &g_val);
}else{ //parent
printf("parent[%d]: %d : %p\n", getpid(), g_val, &g_val);
}
sleep(1);
return 0;
}

[root@localhost d1]# ./testenv
parent[6610]: 0 : 0x601058
child[6611]: 0 : 0x601058

我们发现,输出出来的变量值和地址是一模一样的,很好理解呀,因为子进程按照父进程为模版,父子并没有对变量进行任何修改。可是将代码稍加改动:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int g_val = 0;
int main()
{
pid_t id = fork();
if(id < 0){
perror("fork");
return 0;
}
else if(id == 0){ //child,⼦进程肯定先跑完,也就是⼦进程先修改,完成之后,⽗进程
再读取
g_val=100;
printf("child[%d]: %d : %p\n", getpid(), g_val, &g_val);
}else{ //parent
sleep(3);
printf("parent[%d]: %d : %p\n", getpid(), g_val, &g_val);
}
sleep(1);
return 0;
}

[root@localhost d1]# ./testenv
child[6655]: 100 : 0x601058
parent[6654]: 0 : 0x601058

我们发现,父子进程,输出地址是一致的,但是变量内容不一样!这里我们也可以详细的解答上面fork后if分流之后父子进程之间数据值的不同了。能得出如下结论:

• 变量内容不一样,所以父子进程输出的变量绝对不是同一个变量
• 但地址值是一样的,说明,该地址绝对不是物理地址!
• 在Linux地址下,这种地址叫做 虚拟地址
• 我们在用C/C++语言所看到的地址,全部都是虚拟地址!物理地址,用户一概看不到,由OS统⼀
管理。OS必须负责将 虚拟地址 转化成 物理地址 。

进程地址空间

所以之前说‘程序的地址空间’是不准确的,准确的应该说成 进程地址空间 ,那该如何理解呢?

进程在访问内存的时候,要先进行虚拟地址到物理地址的映射,找到物理内存,然后才可以访问数据。

看图:

分页&虚拟地址空间

上面的图就足矣说明问题,同一个变量,地址相同,其实是虚拟地址相同,内容不同其实是被映
射到了不同的物理地址!

虚拟内存管理

描述linux下进程的地址空间的所有的信息的结构体是 mm_struct (内存描述符)。每个进程只有⼀个mm_struct结构,在每个进程的 task_struct 结构中,有⼀个指向该进程的mm_struct结构体指
针。

struct task_struct
{
/*...*/
struct mm_struct* mm; //对于普通的用户进程来说该字段指向他的虚拟地址空间的用户空间部分,对于内核线程来说这部分为NULL。
struct mm_struct* active_mm; // 该字段是内核线程使用的。当该进程是内核线程时,它的mm字段为NULL,表示没有内存地址空间,可也并不是真正的没有,这是因为所有进程关于内核的映射都是一样的,内核线程可以使用任意进程的地址空间。
/*...*/
}

可以说, mm_struct 结构是对整个用户空间的描述。每⼀个进程都会有自己独立的 mm_struct ,
这样每⼀个进程都会有自己独立的地址空间才能互不干扰。先来看看由 task_struct 到
mm_struct ,进程的地址空间的分布情况:

定位 mm_struct 文件所在位置和 task_struct 所在路径是一样的,不过他们所在文件是不⼀样
的, mm_struct 所在的文件是 mm_types.h

struct mm_struct
{
/*...*/
struct vm_area_struct *mmap;/* 指向虚拟区间(VMA)链表 */
struct rb_root mm_rb;/* 红黑树 */
unsigned long task_size;/*具有该结构体的进程的虚拟地址空间的大小*/
/*...*/
// 代码段、数据段、堆栈段、参数段及环境段的起始和结束地址。
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;
/*...*/
}

那既然每⼀个进程都会有自己独立的 mm_struct ,操作系统肯定是要将这么多进程的 mm_struct
组织起来的!虚拟空间的组织方式有两种:

1.当虚拟区(VMA)较少时,采用链表,由mmap指针指向这个链表。

2.当虚拟区较多时,采用红黑树进行管理,由mm_rb指向这棵树。

linux内核使用vm_area_struct结构来表示一个独立的虚拟内存区域(VMA),由于每个不同的虚拟内存区域功能和内部机制不同,因此一个进程使用多个vm_area_struct结构来分别表示不同类型的虚拟内存区域。

struct vm_area_struct {

        unsigned long vm_start; //虚存区起始
        unsigned long vm_end;//虚存区结束
        struct vm_area_struct *vm_next, *vm_prev;//前后指针(双链表)
        struct rb_node vm_rb;//红黑树中的位置
        unsigned long rb_subtree_gap;
        struct mm_struct *vm_mm;//所属的 mm_struct
        pgprot_t vm_page_prot;
        unsigned long vm_flags;//标志位
        struct {
                        struct rb_node rb;
                        unsigned long rb_subtree_last;
                    } shared;
        struct list_head anon_vma_chain;
        struct anon_vma *anon_vma;
        const struct vm_operations_struct *vm_ops; //vma对应的实际操作
        unsigned long vm_pgoff;//文件映射偏移量
        struct file * vm_file;//映射的文件
        void * vm_private_data;//私有数据
        atomic_long_t swap_readahead_info;
#ifndef CONFIG_MMU
        struct vm_region *vm_region;  /* NOMMU mapping region */
#endif
#ifdef CONFIG_NUMA
        struct mempolicy *vm_policy; /* NUMA policy for the VMA */
#endif
        struct vm_userfaultfd_ctx vm_userfaultfd_ctx;
} __randomize_layout;

mm_struct与VMA的关系:

 为什么要有虚拟地址空间

必备知识:物理内存在内核里的内存管理中,虚拟地址空间在内核里的进程管理中。

这个问题其实可以转化为:如果程序直接可以操作物理内存会造成什么问题?
在早期的计算机中,要运行一个程序,会把这些程序全都装入内存,程序都是直接运行在内存上的,也就是说程序中访问的内存地址都是实际的物理内存地址。当计算机同时运行多个程序时,必须保证这些程序用到的内存总量要小于计算机实际物理内存的大小

那当程序同时运行多个程序时,操作系统是如何为这些程序分配内存的呢?例如某台计算机总的内存大小是128M,现在同时运行两个程序A和B,A需占用内存10M,B需占用内存110M。计算机在给程序分配内存时会采取这样的方法:先将内存中的前10M分配给程序A,接着再从内存中剩余的118M中划分出110M分配给程序B。

这种分配方法可以保证程序A和程序B都能运行,但是这种简单的内存分配策略问题很多。

1.安全风险。

每个进程都可以访问任意的内存空间,这也就意味着任意⼀个进程都能够去读写系统相关内存区域,如果是⼀个木马病毒,那么他就能随意的修改内存空间,让设备直接瘫痪。


 2.地址不确定。

编译完成后的程序是存放在硬盘上的,当运行的时候,需要将程序搬到内存当中去运行,如果直接使用物理地址的话,我们无法确定内存现在使用到哪里了,也就是说拷北的实际内存地址每⼀次运行都是不确定的,比如:第⼀次执行a.out时候,内存当中⼀个进程
都没有运行,所以搬移到内存地址是0x00000000,但是第二次的时候,内存已经有10个进程
在运行了,那执行./a.out的时候,内存地址就不⼀定了。


3.效率低下

如果直接使用物理内存的话,⼀个进程就是作为⼀个整体(内存块)操作的,如果出现物理
内存不够用的时候,我们⼀般的办法是将不常用的进程拷贝到磁盘的交换分区中,好腾出内
存,但是如果是物理地址的话,就需要将整个进程⼀起拷走,这样,在内存和磁盘之间拷贝
时间太长,效率较低。

存在这么多问题,有了虚拟地址空间和分页机制就能解决了吗?当然!

1.地址空间和页表是OS创建并维护的!也就意味着,凡是想使用地址空间和页表进行映射,⼀定要在OS的监管之下来进行访问!!也顺便包括各个进程以及内核的相关有效数据!


2.因为有地址空间的存在和页表的映射的存在,我们的物理内存中可以对未来的数据进行任意位置
的加载!物理内存的分配 和 进程的管理就可以做到没有关系。


3.因为有地址空间的存在,所以我们在C、C++语言上new, malloc空间的时候,其实是在地址
空间上申请的,物理内存可以甚至⼀个字节都不给你。而当你真正进行对物理地址空间访问
的时候,才执行内存的相关管理算法,帮你申请内存,构建页表映射关系(延迟分配),这
是由操作系统自动完成,用户和进程完全0感知!!


4.因为页表的映射的存在,程序在物理内存中理论上就可以任意位置加载。它可以将地址空间上的虚拟地址和物理地址进行映射。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值