前言
上期的分享让我们知道进程大概的模样,本期一样重要,能学到操作系统设计的优美,体会到前辈们的智慧。
#环境变量
是什么
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_profile
,bash
启动时通过它俩加载环境变量。
[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来找文件位置,需要自己指明路径。
【我自己的可执行程序怎么能直接执行?】
-
拷贝到
/usr/bin
下,也就是PATH默认指定的路径(但这样不太好,我们的指令并没有经过严格测试,可能有BUG,更可能污染指令池)
-
给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
可以看到用environ
和char* env[]
打印的结果一模一样。
所以,获取环境变量一般有三种方式: getenv
、char* 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在执行指令的过程中,连物理地址的毛都见不到一根。
为什么把物理内存抽象成虚拟内存再映射?
- 防止越界非法操作
- 对进程共享的代码和数据进行解耦,保证进程独立性
- 共用进程地址空间的内存划分规则,统一标准,能提高效率
以上就是进程概念的全部内容,能算是学习Linux“要翻越的第一座大山”了。
今天的分享就到这里
这里是培根的blog,期待与你共同进步!
下期见~