目录
一.进程控制
1.进程创建
a.Linux 系统中,如何创建一个进程?
b.进程创建成功后,Linux 底层会为其做些什么?
2.进程终止
a.什么是进程终止?
b.进程终止的方法有哪些?
c.exit 与 _exit的区别
3.进程等待
a.什么进程等待?
b.为什么要进程等待?
c.如何进行进程等待?
① wait
② waitpid
二.进程替换
1.单进程版的程序替换
execl
2.理解和掌握程序替的原理
a.单进程的程序替换原理
b.多进程程序替换原理
3.了解程序替换的各函数接口
a. execlp
b. execle
c. execv
d. execvp
4.xshell 父子进程架构和进程替换机制
三.命令行参数和环境变量
1.环境变量
a.环境变量简介
b.常见的环境变量
c.环境变量的全局属性——子进程可继承
d.查看环境变量的方式
e.有关环境变量的指令
2.命令行参数
一.进程控制
1.进程创建
a.Linux 系统中,如何创建一个进程?
① 运行一个可执行程序,就相当于创建了一个新的进程,如:
② 执行 xshell 中的指令(内建命令除外),就相当于在 bash 进程中创建子进程,所以,我们在 xshell 命令行中输入的指令,都相当于创建了一个新的进程,如:
③ fork() 函数创建子进程,该过程会在下文详细讲解。
b.进程创建成功后,Linux 底层会为其做些什么?
Linux 需要对创建出来的进程进行管理,如何管理?先描述,在组织!
即,Linux 会先创建出一个 task_struct 结构体,该结构体内存有相关进程的属性信息,如:进程pid、进程优先级、进程地址空间的入口地址...等。而后将该 task_struct 结构体放入指定的数据结构中,如:运行队列、等待队列...等,等待Linux系统对其进行下一步操作。
与此同时,系统会为进程创建许多额外的资源,如:进程地址空间、用户级页表、文件描述符表...等。
2.进程终止
a.什么是进程终止?
简单来说,就是让进程的状态变成死亡状态,让父进程读取它的状态码和退出信息,而后将进程所占用的资源释放掉,这就是进程终止。
b.进程终止的方法有哪些?
① main() 函数中的 return 0 操作,0 是错误码,表示进程正常结束.
② 任何函数中的 _exit(0) 操作,表示终止整个进程,不再继续执行后续代码!
③ 任何函数中的 exit(0) 操作,表示终止整个进程,不再继续执行后续代码!
④ 在命令行中,用 Ctrl + c,相当于向前台进程发送二号信号,来杀死前台进程。
⑤ 在命令行中,用 kill -9 + pid,相当于向指定进程发送九号信号,来杀死前台进程。
c.exit 与 _exit的区别
1.exit 是库函数,_exit 是系统调用接口。
2.exit 终止进程的时候,会自动刷新缓冲区;_exit 终止进程的时候,不会自动刷新缓冲区。
小知识:exit() 函数在系统调用层面,封装的是 _exit(),而_exit() 执行的是 Linux 内核级别的操作,所以,exit() 终止进程时,所自动刷新的“缓冲区”,绝对不再 Linux 内核中。
至于该缓冲区是啥、又在哪里?博主会在后续的文章中陆续讲到~~
3.进程等待
a.什么进程等待?
--- 通过 wait/waitpid 的方式,让父进程对子进程进行资源回收的等待过程,称为进程等待。
b.为什么要进程等待?
① 可以解决子进程僵尸状态带来的内存泄漏问题;
② 通过进程等待的方式,获取子进程的退出信息;
c.如何进行进程等待?
① wait
pid_t wait(int* status) ;
父进程调用 wait() 函数,如果子进程还没有退出,父进程就需要在wait上进行阻塞等待,直到子进程变成僵尸状态,wait自动回收,然后返回!
参数详解,int* stauts:父进程需要通过两个信号即终止信号和退出信号,得知子进程的运行状态和退出信息,而status负责得到这两个信号。
status 是一个输出型参数,它是如何获取异常信号和退出信号的?
status是一个int类型的整数,32bit,只考虑该int整数的低16位。当一个进程异常了(收到信号),表示该进程执行过程出错,它的退出信息就不重要了。
如何判断有没有收到异常信号? --- 异常信号是否为0,若为0,则无异常;为非0,则抛异常。
获取异常信号(低16位中的低7位):status & 0x7F
获取退出信号(低16位中的高8位): (status >> 8 ) & 0xFF
core dump(核心转储):当进程因为接收到带有core dump属性的信号而终止时,操作系统会将进程在内存中的状态(包括寄存器值、内存内容等)保存到磁盘上的一个文件中,这个文件通常命名为core.PID,其中PID是进程的进程ID。Core dump文件对于后续的调试工作非常有用,因为它提供了进程崩溃时的详细内存映像。
返回值: 若pid_t > 0 则返回等待进程的pid;若pid_t < 0 则返回失败。
② waitpid
pid_t waitpid (pid_t pid, int* status, int options);
第一个参数 pid_t pid :可以指定进程的pid去等待,亦可将pid设为-1,即等待任意进程。
第二个参数 int* status :输出型参数,即传进去的值不重要,传出来的值重要!
第三个参数:int options:为0,表示父进程等待的时候,以阻塞状态等待;为 WNOHANG,表示父进程等待的时候,以非阻塞状态等待。
若阻塞等待,父进程在哪里等子进程?--- 父进程在子进程的等待队列里排队.
阻塞状态 --- 阻塞式调用 --- wait/waited --- 子进程不退出,wait不返回 --- 等待的过程中,父进程啥也做不了!!
非阻塞状态 --- wait/waited不阻塞,而是立即返回 --- 轮询 + 非阻塞方案 --- 可以顺便做一些占据时间并不多的事情
返回值: 若pid_t > 0 则返回等待进程的pid 若pid_t < 0 则返回失败
二.进程替换
我们所创建的所有子进程执行的代码,都是父进程的一部分,那么,如果我们想让子进程执行新的程序(代码和数据不再与父进程有任何关联)呢??
1.单进程版的程序替换
Linux 命令行指令也是一个个进程(内建命令除外),所以我们在代码中,用特殊的函数去调用执行 xshell 命令中的命令(进程),这一过程,本质就是程序替换.
execl
int execl(const char* path, const char* arg, . . .);
第一个参数 path:表示我们要替换的目标程序的路径.
第二个参数 const char* arg:表示目标程序的执行方法.
注意:最终必须以NULL结尾,表示参数传递完毕!!
如:execl("/usr/bin/ls","ls","-a","-l",NULL);
当代码执行到该函数时,本进程的所有数据和代码全都会被替换成目标进程的代码和数据,即使目标进程的代码执行完毕,原先进程的后续代码也将不再执行。
程序替换的函数的调用,调用失败有返回值,调用成功没有返回值!!
示例:
2.理解和掌握程序替的原理
a.单进程的程序替换原理
当原程序代码执行到excel函数接口时,OS会直接将磁盘中目标程序的代码和数据会覆盖原进程映射到物理内存上的代码和数据,并让CPU从“新加载到内存的代码的main函数”开始执行,这期间并不会创建新的PCB和程序地址空间等资源,exec系列的函数调用接口起到的仅仅是“加载器”的作用!
b.多进程程序替换原理
由于父子进程在物理内存上共享代码和数据,所以在磁盘上目标程序的代码和数据覆盖原进程(调用excel函数的父或子进程)在物理内存上的代码和数据前,OS 会发生写时拷贝,即在物理内存上重新拷贝一份代码和数据区,用存放于目标程序的代码和数据。
程序替换并不会创建新的进程,而是让目标进程占用原进程的资源,在原进程资源的基础上运行!
与父子进程间写时拷贝的区别
父子进程间的写时拷贝仅仅拷贝数据区,不会拷贝代码区,父子进程共用代码。而多进程的程序替换既拷贝数据区,也拷贝代码区,以免目标程序的代码将代码区覆盖,影响到第三进程的运行。
子进程怎么知道,新的程序的代码的起始位置在哪?
Linux系统形成的可执行程序是有格式的,是ELF格式,ELF格式中有表头,其中存放进程程序地址空间上各区的入口地址,其中就包括代码段的入口地址(Entry)。
为啥无论是单进程还是多进程 execl() 函数后面的代码都不跑了?
当eip寄存器走到 execl() 函数时,原进程映射到物理内存上的代码和数据都会被目标进程的代码和数据覆盖,并且使eip从目标进程的entry(可执行程序代码段的入口地址)开始执行目标进程的代码,而原进程execl()函数后的代码也就不复存在了。
3.了解程序替换的各函数接口
函数名中带'l'(list)这个字母的,都意味着:该函数的参数是可变参数.
函数名中带'p'(PATH)这个字母的,都意味着:OS会去环境变量向量表中寻找目标程序,所以我们不用再手写程序的路径,直接告诉OS目标程序的程序名即可.
函数名中带'v'(vector)这个字母的,都意味着:参数要以“数组”的形式传递.
a. execlp
① execlp(const char* filename, const char* arg, . . . );
filename:是要执行的程序的程序名.
arg:对目标程序的操作方法.
如:execlp( "ls", "ls","-a","-l", NULL);
b. execle
② execle(const char* path, const char* arg, . . . ,char* const envp[ ]);
path:是要执行的程序的路径.
envp[ ] 是一个指向以null结尾的字符串数组的指针,是给新进程传递的环境变量表!
如:char* const envp[ ]={ "PATH=/bin:/usr/bin", "TZ=UTC", NULL };
execle("/usr/bin/ls", "ls","-a","-l", NULL, envp);
c. execv
execv(const char* path, char* const argv[ ]);
argv 就是将对目标程序的操作,放在一个数组中.
如:char* const argv[ ]={ "ls", "-a", "-l", NULL };
execv( "/usr/bin/ls", argv );
d. execvp
execvp(const char* filename, char* const argv[ ]);
filename:要执行的程序文件的名称。
注意这里只需要文件名,不需要完整路径,因为 execvp会在环境变量指定的目录中查找该程序.
argv:就是将对目标程序的操作,放在一个数组中.
如:char* const argv[ ]={ "ls", "-a", "-l", NULL };
execv( "ls", argv );
程序替换成功后,eip指针指向“目标进程的main函数”使CPU从头开始执行目标进程,而目标进程的main函数中的“命令行参数”是由exec系列函数接口在“加载”目标进程时,传递的参数!!
用excel调用另一个手写的可执行程序
4.xshell 父子进程架构和进程替换机制
当我们启动 xshell 并连接到远端服务器后,我们可以在 xshell 的命令行输入相关指令,此时,我们所在的进程是 bash 进程.
我们输入的命令会由 xshell 发送到远端服务器,在bash进程下进行解析和执行,如果我们执行的命令不是内建命令,bash 进程就会调用 fork() 创建子进程,并在子进程中调用 exec*系列的进程替换接口,由子进程执行完相关代码,并将执行结果交给 bash,而后由 bash 将最终结果交付给我们。
1.什么命令行?? ---一个进程打印出来的一串字符串,包括用户名、主机名、当前路径等信息
2.bash内的命令执行的底层原理??---fork出来的子进程 + exec系列函数 + wait等待
3.环境变量是如给传递给子进程或“替换进程”的??--- 通过exec系列函数传参、地址空间的继承(父子进程)
4.什么是内建命令??---本质是shell内部的一个函数,执行内建命令就是调用shell内部的一个函数
Linux 的命令分类:
① 常规命令,bash fork()创建子进程,让子进程去执行
② 内建命令,bash自己执行,类似于bash调用自己的某个函数去执行
三.命令行参数和环境变量
1.环境变量
a.环境变量简介
定义:环境变量一般是指在操作系统中用来指定操作系统运行环境的一些参数。
如:我们在编写C/C++代码的时候,在链接时,从来不知道我们所链接的动静态库在哪里,但是照样可以链接成功,生成可执行程序,原因就是由相关环境变量帮助编译器进程查找。
环境变量通常具有某些特殊用途,还在系统中通常具有全局属性。
为什么系统自带的指令不需要路径,而我们写的可执行程序运行时需要./ ?
原因:echo #PATH 系统的指令都被添加到了PATH这一环境变量(Linux系统默认的指令搜索路径)中,执行时会自行找到,而我们写的可执行程序的路径并未写到PATH中,所以需要手动输入路径。
b.常见的环境变量
HOME:指定了当前用户的主目录路径
HISTSIZE=1000:表示历史指令最大保存数是1000
SSH_TTY=/dev/pts/25:表示设备终端文件,也就是打印显示器
PATH:可执行程序,也就是指令的搜索路径
LD_LIBRARY_PATH:指定了系统在哪些目录中查找共享库文件,用于链接动态库
修改指定的环境变量,如,修改PATH的方法:PATH=$PATH:/home/lesson15
同理,用覆盖的方法可以删除环境变量。
默认更改环境变量,只限于本次登录,重新登录,环境变量被自动恢复。
c.环境变量的全局属性——子进程可继承
通过命令行参数,打印出所有的环境变量:
int main(int argc, char* argv[ ], char* env[ ])
for(int i=0; env[i] ; i++)
printf("env[%d]->%s\n", i , env[i]);
本地变量 VS 环境变量:本地变量只在bash进程内部有效,不会被子进程继承下去;环境变量通过让所有的子进程继承的方式,实现自身的全局性!
d.查看环境变量的方式
① env (查看所有的环境变量)
② getenv("USER") (通过环境变量名获取某个环境变量的内容)
③ main(int argc,char* argv[],char* env[])传参
④ extern char** environ
e.有关环境变量的指令
① export NAME=123456 (导出本地变量,使其变为一个新的环境变量)
② env | grep NAME (查看某个环境变量)
③ unset NAME (清除某个环境变量或本地变量)
④ set (查看所有本地变量和环境变量)
2.命令行参数
什么是命令行参数?
--- 在命令行中输入的数据,能被main()函数以形参的方式捕捉到,这就是命令行参数。
命令行启动的进程都是shell/bash的子进程,子进程的命令行参数和环境变量都是父进程bash给我们传递的!
那么父进程的环境变量信息又是从哪里来??
我们直接更改的是bash进程内部的环境变量信息,每一次重新登录,都会给我们形成新的bash解释器,并且新的bash解释器自动从?读取形成自己的环境变量信息!
每一次登录的时候,我们的bash进程都会读取 .bash_profile 配置文件中的内容,为我们bash进程形成一张环境变量表信息!
自定义一个环境变量,让我们在每次登录xshell的时候,它都会存在
vim ~/.bash_profile (打开配置文件) ——> 定义一个环境变量PATH 并将该环境变量导出