尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Liunx 操作系统 进程概念(下)

Liunx 操作系统 进程概念(下) 一.命令行参数和环境变量1.环境变量的概念环境变量Environment Variables可以理解成操作系统提前准备好的一些“全局配置信息”程序运行时可以读取这些信息。例如echo $PATH就是查看PATH环境变量。可以把环境变量想象成操作系统 | |--- PATH |--- HOME |--- SHELL |--- ...不同程序都可以读取这些信息。2.常见环境变量1. PATHPATH指定命令的搜索路径。当我们在终端输入一条命令例如ls、python或java时操作系统并不会立刻知道该命令对应的程序存放在哪里。它会按照PATH环境变量中记录的目录顺序依次去这些目录里查找是否存在同名的可执行文件。一旦找到就立即执行如果所有目录都找遍了仍然没有终端就会提示command not found命令未找到。例如当我们执行echo $PATH时通常会看到类似下面这样的输出/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin这里每一段路径之间用冒号:分隔表示系统会按照从左到右的顺序依次在这些目录中搜索命令。这种设计带来的好处是我们不需要记住每个程序安装的完整路径只要把它的目录加入PATH就可以在任何位置直接输入命令名来启动它。在实际开发中我们经常需要修改PATH。比如安装了某个新工具后如果直接运行命令提示找不到通常就是因为它的安装目录还没有被加入PATH。此时可以在终端中临时追加路径例如export PATH$PATH:/自定义/工具目录这条命令会把新的目录追加到现有PATH的末尾从而让系统也能在该目录中查找命令。需要注意的是这种方式只在当前终端会话中生效关闭终端后就会失效如果希望永久生效需要把这一行写入~/.bashrc或~/.zshrc等配置文件里。/bin/ls就能运行因为Linux会去PATH中指定的目录寻找ls。查看echo $PATH可能得到/usr/local/bin:/usr/bin:/bin:/usr/local/sbin这些目录之间用:分隔。Linux执行ls大致会ls ↓ 去PATH中的目录找 ↓ /usr/local/bin/ls ↓ 没找到 ↓ /usr/bin/ls ↓ 找到了 ↓ 执行2.HOMEecho $HOME例如普通用户可能得到/home/whbroot可能是/rootHOME表示当前用户的主目录3.SHELL环境变量查看echo $SHELL可能得到/bin/bash表示当前用户默认使用的Shell。例如/bin/bash就是 Bash。测试假设你写了#include stdio.h int main() { printf(hello world!\n); return 0; }编译gcc hello.c -o hello当前目录hello方法一./hello执行./hello这里.表示当前目录所以./hello就是执行当前目录下的hello程序。因此可以运行。方法二直接输入hello如果输入helloLinux会去PATH里面寻找hello。例如/usr/local/bin /usr/bin /bin ...但是你的hello在当前目录而当前目录通常不在PATH中。所以hello找不到。于是报command not found3.和环境变量相关的命令1.echo—— 查看某一个环境变量语法echo $变量名例如echo $PATH输出/usr/local/bin:/usr/bin:/bin表示查看PATH的值。再比如echo $HOME可能输出/home/whb为什么前面有$$PATH表示把PATH这个变量里面保存的值取出来。所以echo PATH和echo $PATH是不一样的。2.export—— 设置/导出环境变量例如export MYNAMEhello然后echo $MYNAME得到hello这里相当于MYNAME hello并且通过export它成为环境变量子进程也可以继承。例如export TEST123 ./a.outa.out这个程序就可以读取TEST。要区分MYNAMEhello和export MYNAMEhello前者只是 Shell 本地变量。后者是 环境变量可以传给子进程。可以简单记export 把 Shell 变量“导出”给子进程。3.env—— 查看所有环境变量直接输入env会显示很多环境变量例如PATH/usr/local/bin:/usr/bin:/bin HOME/home/whb SHELL/bin/bash USERwhb PWD/home/whb/test所以echo $PATH是只看 PATH而env是看所有环境变量4.unset—— 删除环境变量例如export MYNAMEhello现在echo $MYNAME得到hello然后unset MYNAME再echo $MYNAME就没有值了。所以unset 变量名就是删除这个变量。注意这里不加$unset MYNAME # 正确 unset $MYNAME # 不应该这样写因为unset操作的是变量本身不是变量的值。5.set—— 查看 Shell 变量 环境变量输入set会显示当前 Shell 中定义的变量内容通常很多。可以简单理解成set ↓ Shell变量 环境变量而env ↓ 主要查看环境变量4. 环境变量的组织方式每个程序都会收到一张环境表环境表是一个字符指针数组每个指针指向一个以\0结尾的环境字符串。环境表假设系统有这些环境变量PATH/usr/bin:/bin HOME/home/whb SHELL/bin/bash USERwhb程序运行时这些环境变量可以组织成环境表 | ↓ --------------------- | 指针1 ───────────────┼────→ PATH/usr/bin:/bin\0 --------------------- | 指针2 ───────────────┼────→ HOME/home/whb\0 --------------------- | 指针3 ───────────────┼────→ SHELL/bin/bash\0 --------------------- | 指针4 ───────────────┼────→ USERwhb\0 --------------------- | NULL | ---------------------这就是字符指针数组char *env[]例如环境变量PATH/usr/bin:/bin实际上是一个完整字符串P A T H / u s r / b i n : / b i n \0意思就是env[0] ───→ PATH/usr/bin:/bin\0 env[1] ───→ HOME/home/whb\0 env[2] ───→ SHELL/bin/bash\0所以每个指针指向一个以\0结尾的环境字符串。假设有char *env[] { AAA111, BBB222, CCC333, NULL };最后的NULL作用是告诉程序环境变量已经结束了。类似字符串使用\0表示结束。环境表使用NULL表示结束。所以字符串 字符 → 字符 → 字符 → \0 ↑ 结束 环境表 env[0] → 字符串 env[1] → 字符串 env[2] → 字符串 env[3] → NULL ↑ 结束可以这样遍历环境表Linux程序中通常可以使用extern char **environ;environ指向环境表。例如#include stdio.h extern char **environ; int main() { int i 0; while(environ[i] ! NULL) { printf(%s\n, environ[i]); i; } return 0; }运行后可能输出PATH/usr/local/bin:/usr/bin:/bin HOME/home/whb SHELL/bin/bash USERwhb ...5.通过系统调⽤获取或设置环境变量getenv()函数原型char *getenv(const char *name);作用根据环境变量的名字找到它对应的值。比如我们之前在命令行中echo $PATH可以查看 PATH。在 C 程序里面就可以getenv(PATH)获取 PATH 的值。返回类型char *也就是说返回一个指向环境变量值字符串的字符指针。例如char *p getenv(PATH);那么p ↓ /usr/bin:/bin\0因此可以printf(%s\n, p);6.环境变量通常是具有全局属性的环境变量通常具有全局属性可以被子进程继承下去例如#include stdio.h #include stdlib.h int main() { char *env getenv(MYENV); if(env) { printf(%s\n, env); } return 0; }直接运行./test程序执行char *env getenv(MYENV);但是当前环境中没有MYENVhello world所以getenv(MYENV)返回NULL于是if(env)相当于if(NULL)条件不成立所以printf(%s\n, env);根本不会执行因此没有输出。执行exportexport MYENVhello world现在运行./test./test是 Shell 启动的一个新进程。关系可以简单画成Shell进程 │ │ 创建/启动 ↓ test进程而 Shell 本身已经有MYENVhello world所以启动test时Shell 会把自己的环境变量传递给新程序。于是Shell环境 MYENVhello world ↓ ↓ 继承 ↓ test进程环境 MYENVhello world所以test中getenv(MYENV)就能找到hello world于是输出hello world子进程继承的是父进程的环境但之后它们是相互独立的。例如父进程 MYENVhello ↓ 创建子进程 子进程 MYENVhello如果子进程把自己的环境改成MYENVworld一般不会导致父进程变成MYENVworld父进程仍然是MYENVhello可以理解为创建子进程时子进程获得父进程环境的副本。必须使用export假设你只写MYENVhello world然后./test可能还是没有输出。因为MYENVhello world只是当前 Shell 的本地 Shell 变量不一定会放进传给子进程的环境表。而export MYENVhello world相当于Shell变量 ↓ export ↓ 环境变量 ↓ 加入环境表 ↓ 子进程可以继承这就是导出环境变量二.程序地址空间1.虚拟地址示例1#include stdio.h #include stdlib.h #include sys/types.h #include unistd.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; }可能输出parent[2995]: 0 : 0x80497d8 child[2996]: 0 : 0x80497d8因为fork()后子进程会以父进程为模板拥有相同的虚拟地址空间布局。所以父子进程看到的g_val对应的虚拟地址都是0x80497d8示例2#include stdio.h #include stdlib.h #include sys/types.h #include unistd.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; }可能输出child[3046]: 100 : 0x80497e8 parent[3045]: 0 : 0x80497e8问题来了如果0x80497e8真的是物理地址那么父进程和子进程访问的应该是同一块物理内存。子进程g_val 100;改了它。那么父进程再读取g_val应该也得到100但实际却是父进程0 子进程100所以g_val打印出来的地址不是物理地址。这个地址叫虚拟地址包括我们平时写 C/Cint a 10; printf(%p, a);看到的地址0x7ffffff...是虚拟地址不是内存条上的真实物理地址。真正的物理内存可以简单理解成物理内存 ---------------- | 真实的内存空间 | | | | | ----------------但是普通用户程序不能直接随意操作物理地址。Linux 操作系统负责管理物理内存。程序看到的是程序 ↓ 虚拟地址 ↓ 操作系统 页表 ↓ 物理地址 ↓ 真正的物理内存也就是虚拟地址 ──────→ 物理地址OS必须负责将 虚拟地址转化成 物理地址为什么fork()后一开始还可以共享这里就要引出一个非常重要的机制写时拷贝Copy-On-WriteCOWfork()刚创建子进程的时候操作系统并不会马上把父进程所有内存完整复制一份。而是先让父子进程父进程虚拟地址 ─┐ ├──→ 同一块物理内存 子进程虚拟地址 ─┘例如父进程 子进程 0x80497e8 0x80497e8 │ │ └────────┬─────────────┘ ↓ 物理内存 A g_val 0此时父子进程都没有修改父0 子0所以完全没问题。当子进程执行g_val 100呢这时候事情发生变化。子进程要g_val 100;操作系统发现“这块内存目前和父进程共享子进程现在要写它。”于是触发写时拷贝操作系统给子进程准备一块新的物理内存修改之前 父进程虚拟地址 ──┐ ├──→ 物理内存A 子进程虚拟地址 ──┘ g_val0修改之后父进程 0x80497e8 │ ↓ 物理内存 A g_val 0 子进程 0x80497e8 │ ↓ 物理内存 B g_val 100注意虚拟地址没变仍然都是0x80497e8但是映射到的物理地址变了父虚拟地址 → 物理地址A子虚拟地址 → 物理地址B这就是同一个变量地址相同其实是虚拟地址相同内容不同其实是被映射到了不同的物理地址。2.为什么要设计虚拟地址如果没有虚拟地址所有程序直接使用物理地址程序A ──→ 物理内存 程序B ──→ 物理内存 程序C ──→ 物理内存那么不同程序之间很容易互相覆盖、破坏。有了虚拟地址之后进程A虚拟地址空间 ↓ 页表A ↓ 物理内存 进程B虚拟地址空间 ↓ 页表B ↓ 物理内存每个进程都有自己的进程地址空间。这样可以实现进程之间相互隔离提高内存管理效率实现虚拟内存实现写时拷贝程序不需要知道真实物理地址3.虚拟内存管理描述linux下进程的地址空间的所有的信息的结构体是 个mm_struct结构在每个进程的 mm_struct 内存描述符。每个进程只有⼀ task_struct 结构中有⼀个指向该进程的mm_struct结构体指针。例如struct task_struct { /* ... */ struct mm_struct *mm; /* ... */ };这里struct mm_struct *mm;是一个指针。它指向mm_struct也就是这个进程对应的虚拟地址空间描述信息。可以画成task_struct ┌────────────────────┐ │ PID │ │ 状态 │ │ 优先级 │ │ ... │ │ │ │ mm ────────────────┼────→ mm_struct └────────────────────┘ │ ↓ 虚拟地址空间每⼀个进程都会有⾃⼰独⽴的 mm_struct 这样每⼀个进程都会有⾃⼰独⽴的地址空间才能互不⼲扰。例如进程A 进程B mm_struct A mm_struct B ↓ ↓ 地址空间A 地址空间B ↓ ↓ 虚拟地址0x1000 虚拟地址0x1000 ↓ ↓ 物理内存A 物理内存B虽然进程A0x1000 进程B0x1000可能是一样的虚拟地址但是可以通过不同的页表映射到不同的物理内存。因此不同进程拥有独立的虚拟地址空间从而实现进程之间的内存隔离。4.mm_structstruct mm_struct { struct vm_area_struct *mmap; struct rb_root mm_rb; unsigned long task_size; unsigned long start_code, end_code; unsigned long start_data, end_data; unsigned long start_brk, brk; unsigned long start_stack; unsigned long arg_start, arg_end; unsigned long env_start, env_end; /* ... */ };mm_struct就像一张“进程虚拟地址空间的地图”。它记录了代码在哪里 数据在哪里 堆在哪里 栈在哪里 参数在哪里 环境变量在哪里 有哪些虚拟内存区域例如unsigned long start_code, end_code;表示代码段的开始和结束位置。unsigned long start_data, end_data;表示数据段的开始和结束位置。unsigned long start_brk, brk;和堆heap有关unsigned long start_stack;表示栈的起始位置。unsigned long arg_start, arg_end;表示命令行参数区域。比如./test hello 123这里的hello 123就是命令行参数。unsigned long env_start, env_end;表示环境变量所在区域。5.vm_area_structlinux内核使⽤ vm_area_struct 结构来表⽰⼀个独⽴的虚拟内存区域(VMA)由于每个不同质的虚 拟内存区域功能和内部机制都不同因此⼀个进程使⽤多个vm_area_struct结构来分别表⽰不同类型 的虚拟内存区域。上⾯提到的两种组织⽅式使⽤的就是vm_area_struct结构来连接各个VMA⽅便进 程快速访问一个进程的虚拟地址空间不是一整块完全一样的区域。例如┌───────────────────┐ │ 栈 │ ├───────────────────┤ │ │ │ ... │ ├───────────────────┤ │ 堆 │ ├───────────────────┤ │ 数据段 │ ├───────────────────┤ │ 代码段 │ └───────────────────┘代码、数据、堆、栈的权限用途管理方式都可能不同。所以 Linux 使用struct vm_area_struct来描述一个独立的虚拟内存区域。简称VMAVirtual Memory Area一个进程有很多 VMA比如一个进程进程A │ ↓ mm_struct │ ├── VMA 1 → 代码段 │ ├── VMA 2 → 数据段 │ ├── VMA 3 → 堆 │ ├── VMA 4 → 共享库 │ ├── VMA 5 → 栈 │ └── ...所以mm_struct ↓ 管理整个地址空间 vm_area_struct ↓ 描述其中一个具体的虚拟内存区域vm_area_struct里面最重要的两个成员unsigned long vm_start; unsigned long vm_end;分别表示vm_start → 虚拟内存区域开始地址 vm_end → 虚拟内存区域结束地址比如VMA ┌──────────────────────┐ │ │ │ 某个区域 │ │ │ └──────────────────────┘ ↑ ↑ vm_start vm_end所以一个 VMA 本质上就是从虚拟地址 A 到虚拟地址 B 的这一段区域它是什么、有什么权限、怎么管理。vm_next和vm_prevstruct vm_area_struct *vm_next; struct vm_area_struct *vm_prev;这两个就是前后指针。因此可以把多个 VMA 连接起来VMA1 ←→ VMA2 ←→ VMA3 ←→ VMA4这就是双向链表。而struct vm_area_struct *mmap;可以指向这个链表。所以当虚拟区间较少时可以通过链表管理。如果一个进程有很多 VMAVMA1 VMA2 VMA3 ... VMA100 VMA101 ...如果每次寻找某个地址对应哪个 VMA都从头开始链表查VMA1 → VMA2 → VMA3 → ...效率可能比较低。所以还可以使用红黑树struct rb_root mm_rb;就是红黑树的根。可以简单理解mm_struct / \ 链表 红黑树 ↓ ↓ vm_area_struct vm_area_struct6.为什么一定需要虚拟地址空间如果程序直接操作物理内存会怎么样假设电脑有128MB物理内存程序A需要10MB程序B需要110MB可以分物理内存 0MB │ ├──────── A 10MB ────────┤ │ ├──────── B 110MB ──────────────────────┤ │ └────────────────────────────────────────┘ 128MB看起来能运行。但是这种方式问题非常大。第一个问题安全性如果程序直接操作物理地址程序A ↓ 物理内存那么理论上程序A可能去访问程序B的内存甚至操作系统内核的内存那就非常危险。例如恶意程序修改其他程序数据 ↓ 修改系统数据 ↓ 系统崩溃所以必须进行隔离。有了虚拟地址进程A ↓ 虚拟地址空间A ↓ 页表A ↓ 物理内存程序不能随意访问别人的虚拟地址空间。第二个问题地址不确定假设程序里面有int a 10;如果直接使用物理地址。第一次运行物理地址 0x1000第二次运行由于内存已经被其他程序占用了物理地址 0x5000那么程序每次运行实际地址都可能不同。这对程序非常麻烦。有虚拟地址之后程序只需要认为a → 0x1000每次都可以使用这个虚拟地址。操作系统再负责虚拟地址0x1000 ↓ 页表 ↓ 物理地址可能是0x5000下一次虚拟地址0x1000 ↓ 页表 ↓ 物理地址可能是0x9000程序完全不需要知道。第三个问题提高内存利用率假设物理内存不够。没有虚拟内存时整个进程 ↓ 一起搬到磁盘这样效率很低。有虚拟内存和分页机制以后可以进程的一部分页面 ↓ 物理内存 暂时不用的页面 ↓ 磁盘不需要把整个进程一次性搬走。所以效率更高。7.malloc 申请的是虚拟空间在 C/C 中使用malloc、new申请空间时本质上是向进程的地址空间申请。例如int *p malloc(100 * sizeof(int));你可能以为“操作系统马上给我一块真实物理内存。”实际上不能简单这么理解。可以理解成malloc ↓ 申请虚拟地址空间 ↓ 操作系统建立/准备相应的内存管理关系真正访问页面时如果页面尚未准备好操作系统可能通过缺页异常等机制分配物理页并建立映射。这就是延迟分配。8.“进程管理”和“内存管理”解耦以前如果程序直接使用物理地址程序 ↓ 直接占用物理内存程序和物理内存的位置强相关。有了虚拟地址以后进程 ↓ 虚拟地址空间 ↓ 页表 ↓ 物理内存于是进程只需要关心自己的虚拟地址空间物理内存由操作系统统一管理。这就是进程管理模块和内存管理模块解耦。9.为什么程序看到的地址可以是“连续”的这是虚拟地址非常厉害的地方。例如程序认为虚拟地址 0x1000 0x2000 0x3000 0x4000是连续的。但实际物理内存可能虚拟地址 物理地址 0x1000 → 0x8000 0x2000 → 0x2000 0x3000 → 0xF000 0x4000 → 0x5000物理内存可以是分散的。但是从进程角度看0x1000 ↓ 0x2000 ↓ 0x3000 ↓ 0x4000依然是连续、有序的。所以在进程视角所有的内存分布都可以是有序的。
返回列表