1. 引言
Linux 系统编程中,进程、文件与 Shell 三者紧密交织。很多初学者在学完fork之后,会对exec系列函数、环境变量传递、文件打开机制产生一连串疑问:exec到底会不会创建新进程?环境变量什么时候传给子进程?打开文件时内容真的全部读进内存吗?
本文基于课件笔记整理,用大白话 + 考点的方式,帮你把「Shell 与进程」「文件核心原理」「进程替换 exec」「exec 六大函数」这四块内容一次讲透,并附上考试高频考点与代码示例。
Shell、环境变量 argv env
bash/shell 本身就是一个进程!
你敲指令,shell 的做法:
fork创建子进程,子进程exec去执行你输入的命令。进程的两个数组:
char *argv[]:命令行参数数组(程序名、传入参数)char *env[]:环境变量数组,每个进程都自带一份环境变量表
export myval=111111export 作用:把 shell 里的局部变量,放进 env 环境变量表。
fork 创建子进程的时候,子进程会继承父 shell 的环境变量。
shell 环境变量从哪里来?
用户登录系统,启动 shell 进程,shell 自动读取家目录下的
.bash_profile,里面写好的export命令,自动加载环境变量。.bash_profile:家目录里的一个配置文本文件,开头带点代表隐藏文件,登录自动读取putenv():C 库函数,在程序内部新增 / 修改当前进程的环境变量。
文件的核心原理(大白话 + 考点)
共识原理
文件 = 内容 + 属性
内容:里面存的数据(txt 文字、代码)
属性:文件名、大小、权限、创建时间、位置等信息。
文件分成两大类:已经打开的文件、存磁盘没打开的文件
✅打开的文件:谁打开?进程
一句话:打开文件这件事,一定是某个进程做的。这一块本质就是进程和文件的关系。
进程和打开的文件:1 个进程可以打开 n 个文件(1:n)
文件一旦被打开,要加载到内存,磁盘上的文件本体不动,内存建立内核对象来管理这个打开的文件。
内核里面怎么管理打开的文件:
内核会为每一个被打开的文件创建一个内核结构体(文件打开对象),里面保存这个打开文件的各种属性,用链表串起来管理。
c
运行
struct file { // 文件的各种属性 struct file *next; // 链表指针,把所有打开文件串起来 };没打开的文件:安静躺在**磁盘(硬盘)**上。
重点:磁盘上文件数量巨大,核心需求是快速增删查改,快速找到文件,这就是目录 / 文件系统干的活。
磁盘上整个文件不会一次性全部读到内存!只是一部分信息先进内存
1. 打开文件(open)的时候
当进程调用 open 打开磁盘文件:
内核在内存创建一个
struct file(打开文件对象),记录:文件偏移、权限、状态等加载文件的元数据(属性):大小、权限、位置
✅这些是先进内存的
❌文件里面全部内容,不会一次性读到内存
类比:你打开一本书,只是先拿到这本书的封面、目录信息,不会一次性把整本书全部复印到桌上。
2. 什么时候才把文件内容读到内存?
等到你调用read()读文件的时候:
操作系统才会把需要用到的那一小块数据,从磁盘拷贝到内存缓冲区。
也就是用到哪块,加载哪块(惰性加载)。
3. 区分两个概念(考试重点)
- 磁盘上的文件:永久存在硬盘,关掉电脑还在
- 打开文件的内核对象(struct file):在内存,进程 close 或者进程退出,这个对象就销毁消失
4. 容易混淆的点
课件原话:文件一旦被打开,必须先被加载到内存
课件这句话的真实含义:
代表这个文件已经打开的管理信息(struct file)加载进内存
不是说整个文件全部数据全部搬进内存。
6.2 进程替换
核心一句话(最重要考点)
exec 程序替换,不会创建新进程!只把当前进程的代码、数据全部换成磁盘上新程序的内容,PID 全程不变。
1. 关键问题逐个讲
问题①:程序替换有没有创建新的进程?
❌没有新建进程
- PID(进程编号)不变,
task_struct这个进程基本描述结构体保留不变 - 只有
mm_struct(虚拟地址空间管理结构)里面的旧代码、堆、栈全部清空,加载磁盘上新程序(ELF 可执行文件)
类比:房间不变(房间号 PID 不变),把房间里面旧家具全部清空,搬进一套全新家具。
问题②:环境变量什么时候给进程?exec 的时候会换掉环境变量吗?
课件原话:创建子进程的时候,子进程就继承环境变量。exec 替换不会覆盖环境变量。
流程:
父进程 fork → 子进程诞生,直接复制父进程的环境变量
子进程调用 exec 做程序替换
替换代码、栈、堆,但是环境变量保留不变
✅ 新程序拿到的,还是 fork 继承来的那套环境变量
只有你手动调用带 env 参数的 execve,才可以主动改成新环境变量。
问题③:exec 函数返回值规则(必考)
exec成功的时候,没有返回值!
只有调用失败的时候,才会返回 - 1,继续执行 exec 后面的代码。
大白话:
exec 一旦替换成功,原来程序剩下的代码直接丢掉,再也跑不到。
只有找不到程序、权限不足等失败场景,才会回来继续跑后面代码。
问题④:CPU 怎么找到新程序入口地址?
磁盘上 Linux 可执行程序是ELF 格式
ELF 文件头部里面写好了程序入口地址。
exec(加载器)读取 ELF 头,拿到入口地址,让 CPU 从这个地址开始跑新程序代码。
2. 整体流程(结合 fork+exec,就是 shell 干的事)
shell(父进程)调用 fork,创建子进程
子进程和父进程一模一样,PID 不同,继承环境变量
子进程调用 exec,程序替换
- 子进程 PID 不变,但是代码换成 ls/top 等磁盘上的程序
- 环境变量保留
子进程运行新程序,结束后父进程 wait 回收
3. 课件里的代码片段
c
运行
printf("before: I am a process, pid: %d, ppid:%d\n", getpid(), getppid()); // 程序替换,运行ls命令 execl("/usr/bin/ls", "ls", "-a", "-l", NULL); // 如果execl成功,下面这行永远不会执行!4. 50 行自定义 shell(课件最后目标)
自定义 shell 核心逻辑:
循环:
读取用户输入命令
fork 创建子进程
子进程 exec 执行用户输入的程序
父进程 wait 等待子进程执行结束
循环等待下一条命令。
考点背诵精简版
- exec不创建新进程,PID 不变,task_struct 不变,替换虚拟地址空间里面代码数据
- fork 时继承环境变量;exec 默认保留原有环境变量,不会自动替换
- exec 成功:无返回,后续代码不执行;失败返回 - 1,继续执行后面代码
- Linux 可执行文件 ELF,文件头部保存程序入口地址,exec 加载器读取入口。
exec 六大函数课件
先记住口诀:l、v、p、e
l:list 列表传参,一个个写参数,最后必须补NULLv:vector 数组传参,把全部参数放进一个字符串数组p:path,会自动去PATH环境变量里面搜索程序,不用写完整路径e:environment,自己手动传入新的环境变量数组,替换默认环境变量
6 个 exec 函数:
execl:l 列表传参,需要写完整路径,沿用旧环境变量execlp:l 列表传参,带 p,自动搜 PATH,不用写全路径execle:l 列表传参,带 e,手动给新环境变量execv:v 数组传参,需要写完整路径,沿用旧环境变量execvp:v 数组传参,带 p,自动搜 PATH,不用写全路径execve:v 数组传参,带 e,手动传入环境变量(系统调用,其余 5 个都是库函数,底层都调用 execve)
逐个看图里的例子
① execl(l:列表传参)
c
运行
execl("/usr/bin/ls", "ls", "-a", "-l", NULL);- 第一个参数:程序完整路径
/usr/bin/ls,告诉系统去哪里找这个程序 - 后面一串:命令参数,按顺序写,最后必须以 NULL 结尾
- 参数含义:
ls -a -l,和终端敲命令一模一样 - 没有 p:必须写全路径;没有 e:继承原来进程的环境变量
② execv(v:数组传参)
c
运行
char *const myargv[] = {"ls", "-l", "-a", NULL}; execv("/usr/bin/ls", myargv);- 把所有命令参数放到字符串数组
myargv,数组末尾放 NULL - 适合:参数数量不确定、动态构造参数的场景
- 同样,需要写完整程序路径,沿用旧环境变量
③ execlp(带 p,PATH 搜索)
c
运行
execlp("ls", "ls", "-a", "-l", NULL);✅ 好处:不用写 /usr/bin/ls 完整路径!
它会自动读取PATH环境变量,挨个去 PATH 里面的目录找 ls 程序。
就像你在 shell 直接敲 ls,不用写全路径。
核心考点
- 只要 exec 替换成功,后面代码一律不执行,只有失败才返回 - 1 继续跑下面代码
- 参数数组
argv,最后一个元素必须是 NULL,标记参数结束,漏写会报错 extern char **environ;:全局环境变量,exec 不带 e 的函数,都会默认使用这个环境变量- 只有带
e的 execle /execve,才可以自己传入全新环境变量,覆盖原来的
extern char **environ;术语解释
extern:声明变量,告诉编译器这个变量已经在别的地方定义好了,直接拿来用。
environ是 Linux 系统自带的全局环境变量表。
类型char**:字符串指针数组,存放一堆"key=value"格式的环境变量字符串
核心重点:带e的 exec(execle/execve)
字母e= environment,可以自己传给新程序一套全新的环境变量
不带 e:沿用原来进程的全局
environ环境表带 e:直接用你写的环境数组,覆盖旧环境,不是追加,是整体换掉!
1. 课件里的例子
c
运行
// 自己造一套全新环境变量数组,最后必须NULL结尾 char *const myenv[] = { "MYVAL=1111", "MYPATH=/usr/bin/XXX", NULL }; // execle 末尾把myenv传进去 execle("./otherExe", "otherExe", "-a", "-w", "-v", NULL, myenv);大白话:
运行otherExe这个程序的时候,它拿到的环境变量只有 MYVAL、MYPATH 这两个。
原来进程里的 PATH、HOME 全部消失,被你写的myenv整套替换。
⚠️考点:是覆盖,不是添加!不是在旧环境基础上加这两个变量,直接换成全新一套。
两种给子进程环境变量的方式对比:
- fork 继承 + putenv:父进程先 putenv 增加环境变量,fork 子进程,子进程拿到【旧环境 + 新增变量】(追加)
- execle/execve 传自定义 env 数组:exec 替换程序,直接给一套全新环境,全部覆盖
2. 6 个 exec 函数分类(课件下方 man 手册截图)
c
运行
// 库函数(底层全部调用execve系统调用) int execl(const char *path, const char *arg, ...); int execlp(const char *file, const char *arg, ...); int execle(const char *path, const char *arg, ..., char * const envp[]); int execv(const char *path, char *const argv[]); int execvp(const char *file, char *const argv[]); int execvpe(const char *file, char *const argv[], char *const envp[]); // 真正的系统调用! int execve(const char *filename, char *const argv[], char *const envp[]);一句话:execve 是内核原生系统调用,剩下 6个都是 C 库封装出来的函数,底层最终都会调用 execve
字母口诀复习
l:list,参数一个个罗列,可变参数,末尾 NULLv:vector,参数打包成字符串数组p:PATH,自动去 PATH 环境变量搜索程序,不用写完整路径e:environment,手动传入自定义环境变量数组
3. 思考题:exec 能不能运行我们自己写的程序?
✅可以!不只是能跑ls、top这种系统命令。
只要是编译好的 ELF 可执行文件,不管是系统自带,还是你自己写代码编译出来的./otherExe,exec 都可以加载运行。
完整流程演示
- 你写一段 C 代码,编译生成可执行文件
otherExe - 在主程序里面调用
execle("./otherExe", "otherExe", "-a", NULL, myenv); - 执行后,当前进程代码直接替换成
otherExe,并且otherExe只会看到你给的myenv这组环境变量