Linux——进程的概念(万字总结)

目录

前言

一、 认识冯诺依曼体系结构

二、操作系统

1.什么是操作系统 

2.操作系统的意义

三、进程

1.基本概念 

2.描述进程——PCB 

3.进程和程序的区别

4.test_struct-PCB的一种

5.test_struct的内容分类

四、如何查看进程 

1.通过系统文件查看进程

2.通过ps指令查看进程 

五、如何获取pid和ppid

1.getpid() --- 获取子进程(pid)

2.getppid() --- 获取父进程(ppid)

六、进程的创建 --- fork初识

1.四种主要事件会导致进程的创建

2.用户如何请求创建一个新进程  

3. 如何让父子进程各有所需

七、进程的状态

1.进程状态有哪些

2.进程状态的查看 

3.进程状态的分析 

1.运行状态 --- R

2.浅度睡眠状态 --- S

3.深度睡眠状态 --- D

4.停止状态 --- T

5.僵死状态 --- Z

6.死亡状态 --- X

八、僵尸进程与孤儿进程

1.僵尸进程的危害

2.孤儿进程

九、进程的优先级

1. 基本概念

2.查看进程优先级

3.PRI & NI

4.如何更改进程的优先级

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

2.用renice命令更改已存在进程的nice

5.其他概念

十、环境变量

1.基本概念 

2.常见的环境变量 

3.查看环境变量 

1.测试PATH

2.测试HOME

3.测试SHELL

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

1.echo命令

2.export命令 

3.env命令

4.set命令

5.unset命令

5.环境的组织方式

6.通过代码获取环境变量

1.main函数命令行参数的了解

2.main函数的环境变量获取

3.通过第三方变量environ获取

7.通过系统调用获取环境变量 (常用)

十一、地址空间的阐述 

1.程序地址空间

2.进程地址空间

3.如何通过虚拟地址访问物理地址

总结


前言

        在学习进程之前,我们需要对两个方面的知识有所了解及掌握:冯诺依曼体系结构和操作系统。这两个方面的知识对后续进程的理解尤为重要。

一、 认识冯诺依曼体系结构

根据冯·诺依曼体系结构构成的计算机,必须具有如下功能:

        1.把需要的程序和数据送至计算机中。

        2.必须具有长期记忆程序、数据、中间结果及最终运算结果的能力。

        3.能够完成各种算术、逻辑运算和数据传送等数据加工处理的能力。

        4.能够按照要求将处理结果输出给用户。

为了完成上述的功能,计算机必须具备五大基本组成部件,包括:

        1.输入数据和程序的输入设备;(键盘、磁盘、网卡、显卡、话筒、摄像头等)

        2.输出处理结果的输出设备。(显示器、磁盘、网卡、显卡、音响等)

        3.记忆程序和数据的存储器;(内存)

        4.完成数据加工处理的运算器;(CPU)

        5.控制程序执行的控制器;(CPU)

        从上图的结构可以看出,当输入设备获取到数据信号后,先将其转入内存,经由CPU的处理后,返还给内存,再由输出设备接收,让用户获取到相应的信息。

        总感觉这种过程有些多余,为何不直接将输入设备获取到的数据信号直接由CPU处理,然后传个输出设备呢?

        其原因是两者的存储速度相差太大,但不是说做不到实现CPU与外设打交道,设计出计算机的目的就是为了解决人类难以解决的问题以及快速解决问题的能力。以下是存储器存储速度的金字塔结构:

        内存的存取速度会直接影响计算机的运算速度,由于CPU是高速器件,但是CPU的速度是受制于内存的存取速度的,所以为了解决CPU和内存速度不匹配的问题,在CPU和内存直接设置了一种高速缓冲存储器Cache。 Cache是计算机中的一个高速小容量存储器,其中存放的是CPU近期要执行的指令和数据,其存取速度可以和CPU的速度匹配,一般采用静态RAM充当Cache

二、操作系统

1.什么是操作系统 

         操作系统所处位置操作系统(英语:Operating System,简称OS)是管理和控制计算机硬件与软件资源的计算机程序,是直接运行在“裸机”上的最基本的系统软件,任何其他软件都必须在操作系统的支持下才能运行。

2.操作系统的意义

对上:给用户提供稳定的、高效的、安全的运行环境;

对下:管理好软硬件资源;

如何进行管理呢?(先描述,再组织)

接下来举一个简单的例子:

        在学校有校长、辅导员和学生,三者之前存在什么样的关系呢?校长作为管理者,辅导员作为校长决策的执行者,学生作为被管理者。

        通常都是辅导员管理着学生,校长并不会直接与学生打交道,校长在管理学生方面,是通过数据进行管理的,例如:校长想要知道每个班级的第一名的基本信息并对其奖励,辅导员就开始执行,将自己管理的班级的第一名的学生找出来,将数据发送给校长,校长接收到数据后,确认无误后,将对应的奖励分发给辅导员,再由辅导员分发给学生;这样就做到了管理学生的目的;

        实际上,校长对学生的管理是对学生数据的管理,将这些学生的数据由特定的数据结构链接起来(双链表、树),通过数据结构能够很好对学生相关信息进行增删查改。所以,管理的本质:先描述 被管理者,再对被管理者使用特定的数据结构进行组织。

        

三、进程

1.基本概念 

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

2.描述进程——PCB 

实际上,我们所写的代码在运行起来就是进程,如何管理进程呢?
即先描述,再组织;
        任何进程在形成之时,操作系统要为改进程创建PCB——进程控制模块;简单的将,PCB就是一个结构体( Linux操作系统下的PCB是: struct task_struct ),里面存放了进程相关的属性信息;

3.进程和程序的区别

        首先,我们编写好的程序,在经过编译处理之后,所产生的文件(可执行程序)会放在中,当我们运行程序时,操作系统将磁盘上的文件加载到内存中,同时为它创建PCB(进程控制模块),这两个组合起来才是进程。

4.test_struct-PCB的一种

在Linux中描述进程的结构体叫做task_struct。

task_struct是Linux内核的一种数据结构,它会被装载到RAM(内存)里并且包含着进程的信息

5.test_struct的内容分类

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

四、如何查看进程 

1.通过系统文件查看进程

 通过系统文件/proc,来查看当前所有的进程

[mlg@VM-20-8-centos lesson2]$ ls /proc/

2.通过ps指令查看进程 

        ps指令用于报告当前系统的进程状态。可以搭配kill指令随时中断、删除不必要的程序。ps命令是最基本同时也是非常强大的进程查看命令,使用该命令可以确定有哪些进程正在运行和运行的状态、进程是否结束、进程有没有僵死、哪些进程占用了过多的资源等等,总之大部分信息都是可以通过执行该命令得到的。

[mlg@VM-20-8-centos lesson2]$ ps axj//显示现行终端机下的所有进程,包括其他用户的进程

//显示某一个进程的信息
ps axj | head -1 && ps axj | grep -v grep | grep "myproc" 

五、如何获取pid和ppid

我们想要获取到pid和ppid,就要用到系统调用接口:

pid_t  getpid( void ) --- 返回的是子进程ID

pid_t  getppid( void ) --- 返回的是父进程ID

我们可以通过 man指令对这些函数进行详细说明的查看

1.getpid() --- 获取子进程(pid)

#include<stdio.h>
#include<unistd.h>
int main()
{
	printf("I am child pid: %d\n",getpid());
	return 0;
}

2.getppid() --- 获取父进程(ppid)

#include<stdio.h>
#include<unistd.h>
int main()
{
	printf("I am father pid: %d\n",getppid());
	return 0;
}

这里将两个代码整合到一起后,通过死循环不断的打印有父子进程的ID,并对进程进程检测,发现ps检测到的ID和系统接口获取到的ID是一样的。

六、进程的创建 --- fork初识

1.四种主要事件会导致进程的创建

1.系统初始化

2.正在运行的程序执行了创建程序的系统调用

3.用户请求创建一个新进程

4.一个批处理作业的初始化

2.用户如何请求创建一个新进程  

        通过fork函数来进行进程的创建,我们可以man fork查看相关的函数信息这是一个系统调用,它会创建一个与调用进程相同的副本。在调用fork之后,这两个进程(父进程和子进程)拥有相同的内存映射。

请看下面这段代码,我们在执行循环前创建了一个子进程,会有什么样的效果呢?

#include<stdio.h>
#include<unistd.h>
int main()
{
  fork();//创建子进程
  while(1){
    printf("I am child pid:%d I am father ppid:%d\n",getpid(),getppid());
    sleep(1);
  }
  return 0;
}

        从上述的结果可以看出,main函数的进程和fork创建的进程打印的结果是一样的,并且通过pid和ppid发现,fork的父进程就是main函数的进程,说明fork所创建出来的子进程和父进程在内存上映射。

3. 如何让父子进程各有所需

        以上创建的子进程所做的事和父进程是一样的,显然意义并不大,我们要能够让所创建的子进程做和父进程不一样的事,才是我们想要的。那如何实现呢?

首先,对于fork是有两个返回值的

        1、如果子进程创建成功,在父进程中返回子进程的PID,而在子进程中返回0。
        2、如果子进程创建失败,则在父进程中返回 -1。

#include <stdio.h>  
#include <unistd.h>     
int main()  
{
    int ret = fork();
    if(ret == 0){ //如果子进程创建成功,给子进程返回0
        while(1){                                                                
            printf("I am child!\n");
            sleep(1);
        }
    }
    else if(ret > 0){ //如果子进程创建成功,给父进程返回子进程的pid
        while(1){
        printf("I am father!\n");
        sleep(1);
        }
    }
    else{ //如果子进程创建失败,给父进程返回-1
        //fork error
    }                                                              
    return 0;                                                      
} 

七、进程的状态

1.进程状态有哪些

CPU对进程处理,取决于进程当前进程所处的状态,CPU对于不同状态的进程会采取不同的措施。

/*
* 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  */  //追踪状态,类似于vs下打断点后直接运行到断点处
"X (dead)",           /* 16 */  //死掉的进程
"Z (zombie)",         /* 32 */  //僵尸进程
};
 

2.进程状态的查看 

ps axj / ps aux

3.进程状态的分析 

1.运行状态 --- R

        R运行状态(running): 并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列里。

上图想表达的意思是,进程A处于运行中,在一段时间后,就会切换到进程B....,这个时间很快,CPU运行这些进程是采用了时间轮转调度算法。在时间片轮转调度算法中,系统根据先来先服务的原则,将所有的就绪进程排成一个就绪队列,并且每隔一段时间产生一次中断,激活系统中的进程调度程序,完成一次处理机调度,把处理机分配给就绪队列队首进程,让其执行指令。当时间片结束或进程执行结束,系统再次将cpu分配给队首进程。

2.浅度睡眠状态 --- S

         S睡眠状态(sleeping): 意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡眠 (interruptible sleep))。

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

 处于S状态的进程,是可以被立即终止的

3.深度睡眠状态 --- D

        D磁盘休眠状态(Disk sleep)有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个状态的进程通常会等待IO的结束。例如,当进程要求对磁盘进行写入操作,那么在磁盘进行写入期间,该进程就处于深度睡眠状态,是不会被杀掉的,因为该进程需要等待磁盘的回复(是否写入成功)以做出相应的应答。(磁盘休眠状态)

4.停止状态 --- T

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

#include <stdio.h>
#include <unuistd.h>
int main()
{
    while(1)
    {
        printf("hello linux!\n");
        sleep(1);
    }
    return 0;
}

在上述程序跑起来之后,处于浅度睡眠状态,我们可以发送 SIGSTOP 信号给进程来停止(T)进程;

  

我们发送 SIGSTOP 信号给进程来停止(T)进程后,还可以发送SIGCONT 信号让进程继续运行。

查看kill相关信号:

kill -l

 以上的信号,可以查看教程后,尝试尝试

5.僵死状态 --- Z

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

6.死亡状态 --- X

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

八、僵尸进程与孤儿进程

1.僵尸进程的危害

        有如下代码,我们执行之后,子进程会不断的打印数据,父进程等待子进程的过程中,我们立刻杀掉子进程,那么子进程就会处于僵尸状态,而此时程序还在执行,父进程在等待子进程退出的状态,我们把这种进程称之为僵尸进程。

#include <stdio.h>
#include <unistd.h>
int main()
{
   int ret = fork();
   if(ret == 0){//子进程一直打印
     while(1){
       printf("hello linux!\n");
       sleep(1);
     }
   }
   else{//父进程什么都不干,就睡觉
     sleep(100);                                              
   }
   return 0;
}

僵尸进程的危害:

1.进程的退出状态必须被维持下去,因为他要告诉关心它的进程(父进程),你交给我的任务,我办的怎么样了。可父进程如果一直不读取,那子进程就一直处于Z状态?

        是的!

2.维护退出状态本身就是要用数据维护,也属于进程基本信息,所以保存在task_struct(PCB)中,换句话说,Z状态一直不退出,PCB一直都要维护?
         是的!
3.那一个父进程创建了很多子进程,就是不回收,是不是就会造成内存资源的浪费?
         是的! 因为数据结构对象本身就要占用内存,想想C中定义一个结构体变量(对象),是要在内存的某个位置进行开辟空间!
4.内存泄漏?
         是的!

2.孤儿进程

        刚刚提到了僵尸进程是由于子进程先退出而父进程没有对子进程的退出信息进行读取;那么父进程先退出,子进程在进入僵尸状态后,其父进程未能对其做出处理,那么就称该进程是孤儿进程。若是一直不处理孤儿进程的退出信息,那么孤儿进程就会一直占用资源,此时就会造成内存泄漏。因此,当出现孤儿进程的时候,孤儿进程会被1号init进程领养,此后当孤儿进程进入僵尸状态时就由int进程进行处理回收。

#include <stdio.h>
#include <stdlib.h>                                                     
#include <unistd.h>
int main()
{
   pid_t ret = fork();
   if(ret == 0){//子进程不断的打印数据
     while(1){
       printf("I am child, running!\n");
       sleep(1);
     }
   }
   else{//父进程打印数据,休眠10秒后,直接退出
     printf("father do nothing!\n");
     sleep(10);
     exit(1);
   }
   return 0;
}

九、进程的优先级

1. 基本概念

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

2.查看进程优先级

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

[mlg@VM-20-8-centos lesson2]$ ps -l

我们很容易注意到其中的几个重要信息,有下:
        UID : 代表执行者的身份
        PID : 代表这个进程的代号
        PPID :代表这个进程是由哪个进程发展衍生而来的,亦即父进程的代号
        PRI :代表这个进程可被执行的优先级,其值越小越早被执行
        NI :代表这个进程的nice值 

3.PRI & NI

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

4.如何更改进程的优先级

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

        top命令经常用来监控linux的系统状况,是常用的性能分析工具,能够实时显示系统中各个进程的资源占用情况。

[mlg@VM-20-8-centos lesson2]$ top

想要把ps这个进程的nice值修改了,我们可以在输入top命令后,输入“r”,此时会出现提示:

 在这里输入你想要修改的进程的PID,回车之后

在这里输入你想要修改的nice值,回车之后,按q退出,进行查看

2.用renice命令更改已存在进程的nice

5.其他概念

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

十、环境变量

1.基本概念 

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

2.常见的环境变量 

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

3.查看环境变量 

我们可以通过echo命令来查看环境变量 

[mlg@VM-20-8-centos lesson2]$ echo $PATH

1.测试PATH

        我们在输入指令时(ls / pwd等)并没有输入相应的路劲,只要指令正确就一定能执行,但是我们生成的可执行程序却要加上 ./(当前路径下)才可以被执行。这主要是以为系统在环境变量中找不到你当前可执行程序相应的路径。

        我们通过echo $PATH查看环境变量,发现有许多有路径,并且由(:)号隔开。 当我们输入指令时会通过环境变量查找相应的路径。

如何让我们自己的可执行程序也想系统命令一样直接执行呢?

方法一: 

直接将可执行程序拷贝到里面去,但是不推荐这样做,如果你添加了,忘记删除了,后期在执行某些指令时,突然多出一些东西。污染环境

方法二: 

直接将可执行程序所在路径拷贝到里面去。通过export指令+环境变量的名(export PATH)对环境变量进行新的设置

[mlg@VM-20-8-centos lesson2]$ export PATH=/home/mlg/lesson2

但是这样设置覆盖原来的环境变量,导致很多指令都用不了了

 其实也不用担心,我们只要退出在重新启动就恢复了

正确的做法:

[mlg@VM-20-8-centos ~]$ export PATH=$PATH:/home/mlg/lesson2

2.测试HOME

为什么每次登录不同的用户,所在的家目录都是不一样的,就是因为环境变量HOME

普通用户: 

root用户:

3.测试SHELL

        我们在命令行所输入的指令,都是由命令行解释器进行解释的。我们可以通过查看环境变量SHELL来知道自己当前所用的命令行解释器。

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

5. set: 显示本地定义的shell变量和环境变量

1.echo命令

这个处理可以查看相关的环境的变量,还可以直接打印一些数据 

2.export命令 

设置一个新的环境变量,还可以将本地变量导出环境变量;所谓的本地变量就相当于我们在C/C++中定义一个变量;

3.env命令

显示所有环境变量

4.set命令

显示本地定义的shell变量和环境变量 

5.unset命令

清除环境变量;处理环境变量外,还有本地变量,所谓的本地变量就相当于我们在C/C++中定义一个变量。如下图所示:

5.环境的组织方式

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

6.通过代码获取环境变量

1.main函数命令行参数的了解

对于main函数我们已经很熟悉了,它其实可以带有参数的,你了解吗?main函数有三个参数

int main(int arge, char* argv[], char* envp[])//这里的三个参数就是命令行参数
{}

我们先了解一下前两个参数的作用

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

        结合代码及运行结果可以看出,我们在命令行中只有一个命令,那么对于的数组(argv[])中就有一个元素(字符串“./myproc”),有多个携带的参数就有多个字符串。

我们这里想说明的是,当我们在命令行中敲出一个命令后通过带入不同的参数选项,会有不同的结果。我们通过模拟实现一下这样的效果。

#include <stdio.h>
#include <string.h>
int main(int argc, char* argv[])
{
    if(argc != 2){
        printf("Using: %s -[a|h|ah]\n",argv[0]);
        return 1;
    }
    if(strcmp(argv[1],"-h") == 0){
        printf("hello world!\n");
    }
    else if(strcmp(argv[1],"-a") == 0){
        printf("hello linux!\n");
    }
    else if(strcmp(argv[1],"-ah") == 0){                                        
        printf("hello world!\n");
        printf("hello linux!\n");
    }
    else{
         printf("hello C++!\n");
    }
return 0;
}

 通过上面的代码我们也实现出了通过指令加选项的操作达到不同的效果,指令有很多选项,用来完成同一个命令的不同子功能,选项的底层使用的就是命令行参数。

2.main函数的环境变量获取

main函数除了有命令行参数,还有环境变量,也就是第三个参数。它是用来获取环境变量的

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

3.通过第三方变量environ获取

这是系统提供的一个获取环境变量的第三方变量,是一个二级指针

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

7.通过系统调用获取环境变量 (常用)

以上两种获取环境变量太过于麻烦,我们最长用的还是通过系统调用来获取环境变量。

使用getenv函数获取

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

注:环境变量是具有全局属性的,可以被子进程继承下去

#include <stdio.h>
#include <stdlib.h>
int main()
{
    char * env = getenv("MYENV");
    if(env){
        printf("%s\n", env);
    }
    return 0;
}
直接查看,发现没有结果,说明该环境变量根本不存在
导出环境变量
export MYENV="hello world"
再次运行程序,发现结果有了!说明:环境变量是可以被子进程继承下去的!
         我们的程序执行起来后(进程),其共享父进程bash的环境变量(bash的环境变量是系统给的),那么只有bash的环境变量有所改变,影响的就是全局。

十一、地址空间的阐述 

1.程序地址空间

 对于下面的图大家一定不陌生,接下来通过以下的代码,来正确认识这张图

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h> 
int g_unval;
int g_val = 100;
int main()
{                    
    int a = 10;                                        
    int b = 20;                        
    const char *s = "hello world";       
    printf("code addr:%p\n", main);       //代码区            
    printf("string rdonly addr:%p\n", s); //字符常量区
    printf("uninit addr:%p\n", &g_unval); //未初始化
    printf("init addr:%p\n", &g_val);     //已初始化
    char *heap = (char*)malloc(10);
    printf("heap addr:%p\n", heap);       //堆区
    printf("stack addr:%p\n", &s);        //栈区                         
    printf("stack addr:%p\n", &heap);
    printf("stack addr:%p\n", &a);
    printf("stack addr:%p\n", &b);
    return 0;                           
} 

        通过运行后的结果可以看出,空间所谓的分步情况确实如此,但是接下来这段代码运行后的结果,会让让你很诧异。

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

int g_val = 100;
 
int main()
{
    if(fork() == 0){
        int ret = 5;
        while(ret){                                                                     
            printf("hello--- %d g_val = %d &g_val = %p\n", ret, g_val, &g_val);
            ret--;                         
            sleep(1);
            if(ret == 3){
                printf("################child更改数据###############\n");                              
                g_val = 200;
                printf("#############child更改数据完成##############\n");
            }   
        }                                          
    }                                  
    else{                  
        while(1){                                              
            printf("I am father:g_val = %d &g_val = %p\n", g_val, &g_val);                                                     
        }                                                              
    }                                                              
    return 0;                                               
}

        通过上面的运行结果我们可以总结出:如果C/C++打印出来的地址是物理地址,那么上面的情况绝对不可能出现,所有这里的地址并不是物理地址,而是虚拟地址。

2.进程地址空间

        之前说‘程序的地址空间’是不准确的,准确的应该说成进程虚拟地址空间 ,每个进程都会有自己的地址空间,认为自己独占物理内存。操作系统在描述进程地址空间时,是以结构体的形式描述的,在linux中这种结构体是 struct mm_struct 。它在内核中是一个数据结构类型,具体进程的地址空间变量。

        这些变量就是每个空间的起始位置与结束位置。如下图所示

        进程地址空间就类似于一把尺子,每个空间都有对应的起始位置和结束位置。通过这个虚拟地址去间接访问内存;

为什么不能直接去访问物理内存?

        如果没有进程地址空间的加持,那么程序就会直接访问物理内存,没有区间可言,会存在恶意程序可以随意修改别的进程的内存数据,以达到破坏的目的。有些非恶意的,但是有bug的程序也可能不小心修改了其它程序的内存数据,就会导致其它程序的运行出现异常。这种情况对用户来说是无法容忍的,因为用户希望使用计算机的时候,其中一个任务失败了,至少不能影响其它的任务。

3.如何通过虚拟地址访问物理地址

        每个进程都是独立的虚拟地址空间,两个独立进程的相同地址互不干扰,但是在物理上对每个进程可能也就分了一部分空间给了某个进程。

        每个进程被创建时,其对应的进程控制块和进程虚拟地址空间也会随之被创建。而操作系统可以通过进程的控制块找到其进程地址空间,通过页表对将虚拟地址转换为物理地址,达到访问物理地址的目的。

        这种方式称之为映射,调度某个进程执行时,就要把它的地址空间映射到一个物理空间上。

此时,我们来回答一下刚刚为什么g_val的值发生了变化,但是父进程与子进程的地址还是一样的。

写时拷贝:就是等到修改数据时才真正分配内存空间,这是对程序性能的优化,可以延迟甚至是避免内存拷贝,当然目的就是避免不必要的内存拷贝

总结

        简而言之,首先,程序数据加载到内存后,由操作系统分配进程PCB(test_struct和mm_struct(进程虚拟地址空间))和页表。此时我们的进程就算是创建好了。

虚拟地址的设计有何好处:

        1.有了虚拟地址,每个进程都认为自己独占内存资源,这样对于操作系统来讲,也更加偏于管理进程。

        2.采用间接的地址访问方法访问物理内存。程序中访问的内存地址不再是实际的物理内存地址,而是一个虚拟地址,然后由操作系统将这个虚拟地址映射到适当的物理内存地址上。这样,只要操作系统处理好虚拟地址到物理内存地址的映射,就可以保证不同的程序最终访问的内存地址位于不同的区域,彼此没有重叠。

        3.如果没有进程地址空间的加持,那么程序就会直接访问物理内存,没有区间可言,会存在恶意程序可以随意修改别的进程的内存数据,以达到破坏的目的。反之有利于保护物理内存。

猜你喜欢

转载自blog.csdn.net/sjsjnsjnn/article/details/125533127