【Linux04-进程概念下】不愧是操作系统,优美的设计!

前言

上期的分享让我们知道进程大概的模样,本期一样重要,能学到操作系统设计的优美,体会到前辈们的智慧。


#环境变量

是什么

OS提供,往往有特殊功能的全局变量(/etc/profile.d是设置环境变量的目录)

[bacon@VM-12-5-centos ~]$ cd /etc/profile.d
[bacon@VM-12-5-centos profile.d]$ ll
total 80
-rw-r--r--  1 root root  771 Nov 17  2020 256term.csh
-rw-r--r--  1 root root  841 Nov 17  2020 256term.sh
-rw-r--r--  1 root root 1348 Oct  2  2020 abrt-console-notification.sh
-rw-r--r--  1 root root  660 Apr  1  2020 bash_completion.sh
-rw-r--r--. 1 root root  196 Mar 25  2017 colorgrep.csh
-rw-r--r--. 1 root root  201 Mar 25  2017 colorgrep.sh
-rw-r--r--  1 root root 1741 Nov 16  2020 colorls.csh
-rw-r--r--  1 root root 1606 Nov 16  2020 colorls.sh
-rw-r--r--  1 root root   80 Apr  1  2020 csh.local
-rw-r--r--  1 root root 1706 Nov 17  2020 lang.csh
-rw-r--r--  1 root root 2703 Nov 17  2020 lang.sh
-rw-r--r--. 1 root root  123 Jul 31  2015 less.csh
-rw-r--r--. 1 root root  121 Jul 31  2015 less.sh
-rwxr-xr-x  1 root root  772 Nov 15  2021 mpi-selector.csh
-rwxr-xr-x  1 root root  743 Nov 15  2021 mpi-selector.sh
-rw-r--r--  1 root root   81 Apr  1  2020 sh.local
-rw-r--r--  1 root root  105 Dec 16  2020 vim.csh
-rw-r--r--  1 root root  269 Dec 16  2020 vim.sh
-rw-r--r--. 1 root root  164 Jan 28  2014 which2.csh
-rw-r--r--. 1 root root  169 Jan 28  2014 which2.sh
[bacon@VM-12-5-centos profile.d]$ cat 256term.sh
# Enable 256 color capabilities for appropriate terminals

# Set this variable in your local shell config (such as ~/.bashrc)
# if you want remote xterms connecting to this system, to be sent 256 colors.
# This must be set before reading global initialization such as /etc/bashrc.
#   SEND_256_COLORS_TO_REMOTE=1

# Terminals with any of the following set, support 256 colors (and are local)
local256="$COLORTERM$XTERM_VERSION$ROXTERM_ID$KONSOLE_DBUS_SESSION"

if [ -n "$local256" ] || [ -n "$SEND_256_COLORS_TO_REMOTE" ]; then

  case "$TERM" in
    'xterm') TERM=xterm-256color;;
    'screen') TERM=screen-256color;;
    'Eterm') TERM=Eterm-256color;;
  esac
  export TERM

  if [ -n "$TERMCAP" ] && [ "$TERM" = "screen-256color" ]; then
    TERMCAP=$(echo "$TERMCAP" | sed -e 's/Co#8/Co#256/g')
    export TERMCAP
  fi
fi

unset local256

具体常见的有:

HOME
PATH
SHELL
USER
#...

若要获取某个环境变量的内容如获取PATH的内容:$PATH

[bacon@VM-12-5-centos 8]$ echo $PATH
/usr/local/bin:/usr/bin:/usr/local/sbin:/usr/sbin:/home/bacon/.local/bin:/home/bacon/bin:/home/bacon/linux/4-process/8

环境变量的组织方式

在这里插入图片描述

环境变量的加载

在用户的工作目录下,有.bashrc.bash_profilebash启动时通过它俩加载环境变量。

[bacon@VM-12-5-centos ~]$ pwd
/home/bacon
[bacon@VM-12-5-centos ~]$ cat .bashrc
# .bashrc

# Source global definitions
if [ -f /etc/bashrc ]; then
	. /etc/bashrc
fi

# Uncomment the following line if you don't like systemctl's auto-paging feature:
# export SYSTEMD_PAGER=

# User specific aliases and functions
alias vim='/home/bacon/.VimForCpp/nvim'
alias vim='/home/bacon/.VimForCpp/nvim'
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:~/.VimForCpp/vim/bundle/YCM.so/el7.x86_64
alias vim='/home/bacon/.VimForCpp/nvim'

[bacon@VM-12-5-centos ~]$ cat .bash_profile
# .bash_profile

# Get the aliases and functions
if [ -f ~/.bashrc ]; then
	. ~/.bashrc
fi

# User specific environment and startup programs

PATH=$PATH:$HOME/.local/bin:$HOME/bin

export PATH

有哪些

*命令行变量(局部)
[bacon@VM-12-5-centos 11]$ var=1024 
[bacon@VM-12-5-centos 11]$ echo $var
1024

命令行可以定义局部普通变量,只在当前进程有效(bash)

PATH

:标识指令路径。(默认指定/usr/bin

功能:提供一串或多串路径给系统使用

构成:/a/b/c:/d/e/f:/g/h/i(先找a/b/c,再找d/e/f,最后g/h/i

在这里插入图片描述

读者们或许有过这样的疑惑:系统的可执行程序(指令),和我的可执行程序都是可执行,系统的可执行写出文件名就可以直接执行,而我的可执行就得指定路径。

【我自己的可执行程序为什么要./fileName指定路径?】

要执行一个可执行程序,需要先找到它的位置。

当系统的可执行程序要执行,能且会根据PATH这个环境变量,到/usr/bin下找

当我们自己的可执行程序要执行,不会根据PATH来找文件位置,需要自己指明路径

【我自己的可执行程序怎么能直接执行?】
  1. 拷贝到 /usr/bin 下,也就是PATH默认指定的路径

    (但这样不太好,我们的指令并没有经过严格测试,可能有BUG,更可能污染指令池)

  2. 给PATH追加另外的路径(我们希望直接执行的程序的路径)

    [bacon@VM-12-5-centos 8]$ ls
    myHello  myHello.c
    [bacon@VM-12-5-centos 8]$ myHello
    -bash: myHello: command not found
    [bacon@VM-12-5-centos 8]$ pwd
    /home/bacon/linux/4-process/8
    [bacon@VM-12-5-centos 8]$ export PATH=$PATH:/home/bacon/linux/4-process/8 #export导入原来的环境变量
    [bacon@VM-12-5-centos 8]$ myHello
    myHello:hello hello hello!
    

    如果没有$PATH来获取原来的环境变量再追加,就会覆盖原PATH,导致大部分系统指令不可用。不过这里的PATH是内存级的(每次bash启动时加载到内存),重启即可重置。

PWD

:当前路径

功能:提供当前路径给系统使用

[bacon@VM-12-5-centos ~]$ echo $PWD
/home/bacon

ls指令显示当前路径下的内容,怎么知道当前路经是什么?

[bacon@VM-12-5-centos 12]$ ls
makefile  mycmd  mycmd.c
[bacon@VM-12-5-centos 12]$ env | grep PWD
OLDPWD=/home/bacon/linux/4-process
PWD=/home/bacon/linux/4-process/12

就是依靠PWD环境变量。

HOME

:家目录

[bacon@VM-12-5-centos ~]$ echo $HOME
/home/bacon

特性

我们使用中发现,bash的子进程,都可以使用bash启动时加载的环境变量:

  • 环境变量具有全局属性(子进程可以从父进程继承环境变量,应对不同场景,如USER确定身份)
  • 本地变量只在当前进程有效

怎么操作

env:查看所有环境变量

[bacon@VM-12-5-centos ~]$ env
XDG_SESSION_ID=76445
HOSTNAME=VM-12-5-centos
TERM=xterm
SHELL=/bin/bash
HISTSIZE=3000
SSH_CLIENT=113.90.31.45 51003 22
SSH_TTY=/dev/pts/0
USER=bacon
#...

$[环境变量名]:获取环境变量的值

[bacon@VM-12-5-centos ~]$ echo $PATH
/usr/local/bin:/usr/bin:/usr/local/sbin:/usr/sbin:/home/bacon/.local/bin:/home/bacon/bin

getenv:获取环境变量

#include <stdlib.h> 

char *getenv(const char *name);

The getenv() function searches the environment list to find the environment variable name, and returns a pointer to the corresponding value string.

可以实现一个简单的身份验证:

#define USER "USER"

int main()
{
    const char* who = getenv(USER);

    if(strcmp(who, "root") == 0)
    {
        printf("%s\n", who);
    }
    else 
    {
        printf("permisson denied!\n");
    }


    return 0;
}
[bacon@VM-12-5-centos 12]$ ./mycmd 
permisson denied!
[bacon@VM-12-5-centos 12]$ sudo ./mycmd 
root

sudo的本质其实就是把环境变量USERNAME改成root

export [环境变量名]:查看、新增、修改或删除环境变量(此次登陆有效)

[bacon@VM-12-5-centos 12]$ export MYENV="hello ENV"
[bacon@VM-12-5-centos 12]$ export | grep MYENV
declare -x MYENV="hello ENV"
[bacon@VM-12-5-centos 12]$ env | grep MYENV
MYENV=hello ENV

成功创建环境变量。

set:显示本地变量和环境变量

[bacon@VM-12-5-centos 12]$ MYENV=123123
[bacon@VM-12-5-centos 12]$ env | grep MYENV #env查不到本地变量
[bacon@VM-12-5-centos 12]$ set | grep MYENV #set可以
MYENV=123123

unset:清除环境变量

[bacon@VM-12-5-centos 12]$ MYENV_=456456
[bacon@VM-12-5-centos 12]$ export MYENV_
[bacon@VM-12-5-centos 12]$ env | grep MYENV_
MYENV_=456456
[bacon@VM-12-5-centos 12]$ unset MYENV_
[bacon@VM-12-5-centos 12]$ env | grep MYENV_
#通过代码获取环境变量

1、通过命令行参数获取

读者朋友们有没有见过main函数的参数呢?现在需要拿出来玩玩了!

int main(int argc, char* argv[], char* env) { ... }

我们先看看前两个关于命令行参数的参数

argc = argument count 参数个数

argv = argument vector 参数数组

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;
}
[bacon@VM-12-5-centos 13]$ ./mycmd 
argv[0] = ./mycmd
[bacon@VM-12-5-centos 13]$ ./mycmd -a
argv[0] = ./mycmd
argv[1] = -a
[bacon@VM-12-5-centos 13]$ ./mycmd -a -b
argv[0] = ./mycmd
argv[1] = -a
argv[2] = -b

看完也能知道:

命令行操作就是一个字符串,命令行解释器在进行解析的时候,会将其从左到右按空格分割,成为 “指令”、“参数”等。

"ls -a -b -c -d -e"
=
"ls" "-a" "-b" "-c" "-d" "-e"

在这里插入图片描述

命令行参数的意义是什么呢?

//./mycmd -a/-b/-c

int main(int argc, char* argv[])
{
    if(argc != 2)
    {
        printf("Usage:\n\t%s[-a/-b/-c]\n", argv[0]);
        return 1;
    }

    if(strcmp("-a", argv[1]) == 0)
        printf("fuction a...\n");

    if(strcmp("-b", argv[1]) == 0)
        printf("fuction b...\n");

    if(strcmp("-c", argv[1]) == 0)
        printf("fuction c...\n");

    return 0;
}
[bacon@VM-12-5-centos 13]$ ./mycmd 
Usage:
	./mycmd[-a/-b/-c]
[bacon@VM-12-5-centos 13]$ ./mycmd -a
fuction a...
[bacon@VM-12-5-centos 13]$ ./mycmd -b
fuction b...
[bacon@VM-12-5-centos 13]$ ./mycmd -c
fuction c...

命令行参数的意义:通过传入的参数,执行不同的功能。

main的参数除了以上两个,还有env,环境变量。

*环境变量其实就是字符串,如PATH=/usr/bin,等号左边的PATH就是变量名,右边的/usr/bin就是值

在这里插入图片描述

int main(int argc, char* argv[], char* env[])
{
    int i = 0;
    for(i = 0; env[i]; ++i)
    {
        printf("env[%d] = %s\n", i, env[i]);
    }
  	return 0;
}
[bacon@VM-12-5-centos 13]$ ./mycmd 
env[0] = XDG_SESSION_ID=84774
env[1] = HOSTNAME=VM-12-5-centos
env[2] = TERM=xterm
env[3] = SHELL=/bin/bash
env[4] = HISTSIZE=3000
#...

还有一点需要注意:以上的命令行参数表和环境变量表,不管有没有手动传参,都会生成。

2、environ

#include <unistd.h>
extern char **environ; //全局二级指针

environ 指向 char* env[]

int main()
{
    extern char** environ;
    int i = 0;
    for(i = 0; environ[i]; ++i)
    {
        printf("*(environ+%d) = %s\n", i, environ[i]);
    }
    return 0;
}
[bacon@VM-12-5-centos 13]$ ./mycmd 
*(environ+0) = XDG_SESSION_ID=84774
*(environ+1) = HOSTNAME=VM-12-5-centos
*(environ+2) = TERM=xterm
*(environ+3) = SHELL=/bin/bash
*(environ+4) = HISTSIZE=3000

可以看到用environchar* env[]打印的结果一模一样。


所以,获取环境变量一般有三种方式: getenvchar* env[]extern char** environ

最推荐getenv,按需获取,其他两种方式获取完还要做字符串解析,不方便。

#有趣的现象
[bacon@VM-12-5-centos 13]$ myval=8848
[bacon@VM-12-5-centos 13]$ env | grep myval
[bacon@VM-12-5-centos 13]$ set | grep myval
myval=8848
[bacon@VM-12-5-centos 13]$ echo $myval
8848

我们知道,环境变量是具有全局属性的,但myval是本地变量,不能被echo这个子进程继承,那是如何打印出结果的?

按下不表,回头解答。


#进程地址空间(重要)

学习C语言的时候,我们浅浅了解过C/C++内存分布,其实也叫做进程地址空间:

在这里插入图片描述

验证:

int uninit_var;
int init_var = 100;

int main(int argc, char* argv[], char* env[])
{
    //环境变量
    printf("%-15s = %p\n", "env", getenv("PATH"));

    //命令行参数
    printf("%-15s = %p\n", "cmdLine arg", &argv[0]);

    //栈
    int var_first = 0;
    int var_second = 0;
    printf("%-15s = %p\n", "stack_first", &var_first);
    printf("%-15s = %p\n", "stack_second", &var_second);
    
    //堆
    int* p = (int*)malloc(sizeof(int));
    printf("%-15s = %p\n", "heap", p);

    //未初始化数据
    printf("%-15s = %p\n", "uninitData", &uninit_var);

    //已初始化数据
    printf("%-15s = %p\n", "initData", &init_var);

    //代码段
    printf("%-15s = %p\n", "code", main);

    return 0;
}
[bacon@VM-12-5-centos 15]$ ./mycmd 
env             = 0x7ffe21898e14
cmdLine arg     = 0x7ffe218978e8
stack_first     = 0x7ffe218977f4
stack_second    = 0x7ffe218977f0
heap            = 0x1c7b010
uninitData      = 0x60104c
initData        = 0x601044
code            = 0x4005cd

在这里插入图片描述

*等下的讲解中,我们将对地址空间进行部分简化来降低学习成本。

之前我们认为这就是内存,内存就是这。其实不然,这是“虚拟内存”。来看一个现象。

int global_var = 100;

int main()
{
    pid_t id = fork();
    assert(id >= 0);

    if(id > 0)
    {
        while(1)
        {
            printf("PARENT process:id=%d\tpid=%d\tppid=%d\t|\tglobal_var=%d\t&global_var=%p\n", id, getpid(), getppid(), global_var, &global_var);
            sleep(1);
        }
    }
    else
    {
        int cnt = 0;
        while(1)
        {
            printf("CHILD  process:id=%d\tpid=%d\tppid=%d\t|\tglobal_var=%d\t&global_var=%p\n", id, getpid(), getppid(), global_var, &global_var);
            sleep(1);
            if(cnt == 3)
            {
                global_var = 999;
                printf("global_var changed to 999!\n");
            }
            ++cnt;
        }
    }
    return 0;
}
[bacon@VM-12-5-centos 14]$ ./mycmd 
PARENT process:id=8414		pid=8413	ppid=2621	|	global_var=100	&global_var=0x601064
CHILD  process:id=0			pid=8414	ppid=8413	|	global_var=100	&global_var=0x601064
PARENT process:id=8414		pid=8413	ppid=2621	|	global_var=100	&global_var=0x601064
CHILD  process:id=0			pid=8414	ppid=8413	|	global_var=100	&global_var=0x601064
PARENT process:id=8414		pid=8413	ppid=2621	|	global_var=100	&global_var=0x601064
CHILD  process:id=0			pid=8414	ppid=8413	|	global_var=100	&global_var=0x601064
PARENT process:id=8414		pid=8413	ppid=2621	|	global_var=100	&global_var=0x601064
CHILD  process:id=0			pid=8414	ppid=8413	|	global_var=100	&global_var=0x601064
PARENT process:id=8414		pid=8413	ppid=2621	|	global_var=100	&global_var=0x601064
global_var changed to 999!
CHILD  process:id=0			pid=8414	ppid=8413	|	global_var=999	&global_var=0x601064
PARENT process:id=8414		pid=8413	ppid=2621	|	global_var=100	&global_var=0x601064
CHILD  process:id=0			pid=8414	ppid=8413	|	global_var=999	&global_var=0x601064
PARENT process:id=8414		pid=8413	ppid=2621	|	global_var=100	&global_var=0x601064
CHILD  process:id=0			pid=8414	ppid=8413	|	global_var=999	&global_var=0x601064
PARENT process:id=8414		pid=8413	ppid=2621	|	global_var=100	&global_var=0x601064
CHILD  process:id=0			pid=8414	ppid=8413	|	global_var=999	&global_var=0x601064

在这里插入图片描述

相同的地址,代表父子进程打印的是同一个变量。但同一个变量居然有不同的值?

那我们能推导:这里打印的地址绝对不是物理地址 ==> 曾经学的语言层面的地址(指针),不是物理地址。

这里的地址是 虚拟地址,以前学的内存分布其实叫做 进程地址空间

要把这个现象搞清楚,我们得先学习进程地址空间。


地址空间是什么呢?

感性理解

其实操作系统给进程画了一张大饼。怎么理解呢?

大富翁张三有10个亿的资产和三个私生子张小一、张小二、张小三,且三个儿子互不知道对方的存在。

张小一是搞工程的,张小二是搞投资的,张小三还在国外念书。张三跟每个孩子都是:“孩子啊,好好工作,爸就你一个孩子,那10个亿的资产迟早是你的。”

张小一安心了,专心搞工程,偶尔工程需要资金周转,会找张三要个几万几十万;张小二也安心,专心研究自己的投资,偶尔需要了,也会找张三要个几万;张小三也很安心,乖乖念书,正常开销。

就这样,张三实际上花不了多少钱,却也能让儿子们安心工作学习。孩子们都以为自己有10个亿,其实没有。

着眼进程,操作系统给进程们画了张大饼:操作系统告诉进程它独占系统资源。进程就安心了,跑自己的程序,偶尔请求点资源。

操作系统画的这块大饼,是进程看到的地址空间,也叫做进程地址空间

在这里插入图片描述

*简化后的地址空间和内存

每个进程都以为自己不愁吃穿家财万贯,不过只是不愁吃穿,家里有多少钱自己都搞不清。

给进程创建的地址空间多了,要不要被操作系统管理起来呢?要的。一样的四步走:抽象、具象、组织和操作。

地址空间的抽象:mm_struct

32位机器下,地址有2^32个,被分成栈取、堆区等。而所谓划分地址区域其实就是画了根三八线,怎么理解?

通过小胖和小美的例子来理解:

小胖和小美是一男一女两个小学生,互为同桌。但小胖体型较宽,又坐不住,手就老是伸到了小美的桌子上,很打扰小美的学习,小美就提出要画三八线。

在这里插入图片描述

这时候可以这样描述桌子…

伪代码:

struct disk
{
  	size_t boy_start;
  	size_t boy_finish;
  	size_t girl_start;
  	size_t girl_finish;
}

struct disk = {1, 50, 51, 100};

但小胖还是一直越界,小美忍了一段时间终于忍不下去了,于是大发雷霆,跟小胖重新画线:

在这里插入图片描述

伪代码:

disk->boy_finish = 35;
disk->girl_start = 36;

划分好了以后,怎么用就是自己的事了,小胖可以在[1,5]放铅笔,[7,11]放红领巾。

在这里插入图片描述

尺子上的每一个刻度,就代表桌子上的一个位置,地址就是这么回事,就是给一个区域划分的。

其实内存的划分和调整,和这个三八线的划分调整一样。比如动态变化的栈和堆,变化的本质其实就是调整规定区域的数字stack_finish/heap_finish

伪代码:

struct mm_struct
{
	unsigned int code_start, code_finish; //代码段
  	unsigned int data_start, data_finish; //数据段
	unsigned int heap_start, heap_finish; //堆区
  	unsigned int stack_start, stack_finish; //栈区
  	//...
}

mm_struct也是task_struct中的一个成员:新建一个进程时,需要给它创建进程地址空间

不信看看源码:

struct task_struct {
	volatile long state;	/* -1 unrunnable, 0 runnable, >0 stopped */
	void *stack;
	atomic_t usage;
	unsigned int flags;	/* per process flags, defined below */
	unsigned int ptrace;

	int lock_depth;		/* BKL lock depth */
  
	//...
	
  	//指向为进程创建的地址空间
	struct mm_struct *mm, *active_mm;
  
  //...
}

struct mm_struct {
	struct vm_area_struct * mmap;		/* list of VMAs */
	struct rb_root mm_rb;

	//...
  
  //区域划分
	unsigned long total_vm, locked_vm, shared_vm, exec_vm;
	unsigned long stack_vm, reserved_vm, def_flags, nr_ptes;
	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;
  
  //...
 
}

所以,创建进程地址空间很像是这样的…

伪代码:

struct task_struct task1;

struct mm_struct* pointer = (struct mm_struct*)malloc(sizeof(struct mm_struct));
pointer->code_start = 0x1000;
pointer->code_finish = 0x1100;
pointer->data_start = 0x1200;
pointer->data_finish = 0x1300;

task1->mm = pointer;

//...

进程的视角:嗯…代码段数据段,各区域都很齐全!安心跑我的。

操作系统的视角:这个进程运行用不了多少资源,这一小块用来画大饼都足够了。

进程以为自己独占资源,真正占多少资源还是操作系统决定的。

感性理解进程地址空间:进程地址空间就是操作系统给进程画的大饼,有限的内存给每个进程都划分自己的代码段、数据段等。

理性理解

有了上面的了解,大概能理清一些操作系统软件层面的内存和硬件层面的物理内存

在这里插入图片描述

*简化后的地址空间、物理内存和磁盘

软件和硬件之间,到底怎么转换的,虚拟内存和物理内存又有什么联系?

对待物理内存,我们还能更进一步了解:

在这里插入图片描述

  • 内存和磁盘之间的读写称为 IO操作(input output)

    • 每次IO的基本单位是4KB

    • 每个4KB又叫做 page 页

      因此我们可以将内存看作一个元素为page的数组,对于32位机器,有2^32个地址,能表示4GB的空间,这个数组的大小就是 4GB/4KB

有了这层理解,我再告诉你,虚拟地址和物理地址通过页表映射

在这里插入图片描述

先不用太关心页表到底是个啥,很复杂,我们目前只需要知道:

进程创建时会有自己的页表。变量在内存中的地址,都是虚拟地址,但变量本身一定是存在物理地址上,二者通过进程自己的页表的映射建立联系。

这样一来,我们就可以通过虚拟地址访问物理内存了。抽象后的虚拟地址,因其连续性,也叫线性地址。

理性理解进程地址空间:进程地址空间就是将物理内存抽象再具象,得到的虚拟地址,和物理地址通过页表映射建立联系。


再来解决一下遗留问题:相同的地址,不同的值。

在这里插入图片描述

在这里插入图片描述

但不对劲了啊,如果子进程里面想对这块空间写入,如 global_var = 999,就会影响到父进程看到的global_var,假如父进程里面有这样的代码:

if(global_var == 100) 
{
		//...
}

就会被彻底影响——进程独立性不存在了!

为了保证进程独立性,若被多个进程共享的数据要被修改,会进行一种操作:写时拷贝

即,在物理内存上找一块空间拷贝一份数据,这一块物理内存的地址通过页表的映射,虚拟地址和原数据的一模一样。

在这里插入图片描述

这也能解释为什么同一地址会有不同值:原数据在物理内存的另一块空间上被拷贝了一份,但新空间的虚拟地址通过页表映射后,和原数据的虚拟地址相同,本质他们已经是两块不同的空间了!

“共享数据和代码”,只是一种提升效率的方式,只读的时候,就无需拷贝一份给子进程;若要写入,再拷贝。简单说,不写入的话,不拷贝,血赚;写入的话,才拷贝,不亏。所以之前说的“父子进程共享数据和代码”只是一种提高效率的方式,本质上还是独立的。


为什么

你讲了这么多,我们到底要这么多复杂的东西干嘛,为什么要区分出虚拟地址和物理地址,这有什么好处吗?

原因一:越界非法操作不安全。

在这里插入图片描述

这岂不是完蛋了嘛。

那进程地址空间怎么解决这个问题的?通过压岁钱例子来理解:

7岁的张三过年收到很多压岁钱,父母不管他,让他开心花,最后花了很多冤枉钱买了一堆可能根本就不喜欢的东西,张三开心两天也觉得没意思了。

张三的父母发现这个问题……决定下一年帮他保管试试看。

又过年了,8岁的张三还是收到很多压岁钱,但父母会帮他保管,想买什么了,跟父母一说,合理就买,不合理就不买。最后这钱花了很久,买的都是有意思的东西,张三也开心了很久。

“不合理就不买”,本质上就是一种保护。

具体到进程地址空间上

进程地址空间解决越界非法操作:进程地址空间 + 页表映射的设计,使得访问内存必须经过页表的一次映射,映射过程就可以拦截非法操作

我们总写的野指针,越界访问,都没让我们的操作系统崩溃,就是有页表的保护。


原因二:对进程进行解耦,更好地保证进程独立性。

进程地址空间对进程解耦:

  • 结构上:
    进程地址空间的存在,使得每个进程都有自己的_task_struct地址空间页表等,在进程相关的所有数据结构上互相独立

  • 数据上:
    若有进程间共享同一份数据也不影响,进程地址空间 + 页表映射的设计,使得一旦有写入这种影响进程独立性的操作,能检测到,随后写实拷贝!进程间的数据也互相独立

进程间的数据结构独立,数据独立,进程本身就互相独立!


原因三:共用进程地址空间的内存划分规则,统一标准,能提高效率。

想一个问题:磁盘里的可执行程序里面,有没有地址呢?或者说需不需要地址这种东西的存在呢?

有的,需要的。我们以前看汇编,是有地址的,比如编译的最后一步,链接。链接本质到底是在做什么?说白了就是把“外部符号”都变成“已知符号”,把原本无意义的占位地址替换成符号对应的真正的地址。

也就意味着,程序没有加载到内存的时候,内部就早已经有地址啦!

而且,进程地址空间的一套区域划分规则,不是只有操作系统在用的!!程序编译的时候也会用!!

所以,所谓链接时“真正的地址”又是什么呢?就是编译器根据进程地址空间的划分规则编出来的地址,也叫逻辑地址

在这里插入图片描述

当sort.exe加载进内存,这些地址根本不用变!

  • sort.exe载入,页表为其各个虚拟地址建立映射,和物理地址联系起来

同时

  • 代码段可以通过main的地址确定code_start(因为main是程序的入口);也可以通过程序的结束地址确定code_finish(是程序的结束)

在这里插入图片描述

因此,程序内的地址(逻辑地址/虚拟地址)载入内存后,CPU拿来就能用。

而且,物理内存中的 main()quickSort()arr[]都具有两套地址,一套是载入内存天然具有的物理地址,一套是编译时的逻辑地址(虚拟地址)。CPU在执行指令的过程中,连物理地址的毛都见不到一根。


为什么把物理内存抽象成虚拟内存再映射?

  1. 防止越界非法操作
  2. 对进程共享的代码和数据进行解耦,保证进程独立性
  3. 共用进程地址空间的内存划分规则,统一标准,能提高效率

以上就是进程概念的全部内容,能算是学习Linux“要翻越的第一座大山”了。


今天的分享就到这里

这里是培根的blog,期待与你共同进步!

下期见~

  • 12
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 14
    评论
评论 14
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

周杰偷奶茶

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值