当前位置: 首页 > news >正文

进程的控制

目录

1.进程创建

1.1fork初识

1.2写时拷贝

1.3fork常规用法

1.4fork失败

2.进程终止

2.1进程终止方法

2.2.1退出码

2.2.2_exit终止进程

2.2.3exit函数终止进程

2.2.4return

2.2.5errno

2.3进程退出场景

3.进程等待

3.1进程等待必要性

3.2进程等待方法

3.2.1wait

3.2.2waitpid

3.2.3获取子进程status

3.2.4阻塞和非阻塞等待

4.进程程序替换

4.1原理

4.2替换函数

5.自定义shell


1.进程创建

1.1fork初识

前面进程概念里讲得差不多了。这里不多说了。

在linux 中 fork函数是非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,
而原进程为父进程。fork创建子进程就是在内核中通过调用clone(也是个系统调用)实现,clone函数的功能是创建一个pcb,fork创建进程以及后边的创建线程本质内部调用的clone函数实现

#include <unistd.h>
pid_t fork(void);
返回值:子进程中返回0,父进程返回子进程id,出错返回-11. 为什么要给子进程返回0,父进程返回子进程pid?
2. 为甚一个函数fork会有两个返回值?
3. 为什么一个id即等于0,又大于o?
这三个问题,前2个问题,我前面进程概念中讲fork的时候已经讲了,第三个问题通过对进程地址空间的说明也解释了。

进程调用fork 代码后,当控制转移到内核中的fork(系统调用),内核做:

        分配新的内存块和内核数据结构给子进程

        将父进程部分数据结构内容拷贝至子进程

        添加子进程到系统进程列表当中

        fork返回,开始调度器调度

注意,进程是在用户空间的,fork函数的系统调用是在内核空间中的。

fork之后谁先执行,由调度器决定。

子进程还会继承父进程的环境变量和当前工作目录

1.2写时拷贝

前面进程概念也讲得差不多了。这里就放个网图,形象的描述下写时拷贝。

        为什么os不直接把父进程的数据在子进程创建的时候就直接全部拷贝一份出来呢

        很明显,按需分配空间,只要子进程不需要修改数据,那么就没必要为其单独开辟空间,可以减少不必要的拷贝工作。

        为什么写时拷贝,需要先拷贝一份旧数据,再进行写入而不是申请个没有数据的空间呢

        因为写入,不代表覆盖了旧数据,假如有个变量a,我们进行了a++的操作,这个操作的前提就是要有旧数据,才能在旧数据的基础上进行修改。

        

对于页表,再稍微拓展一点。

        页表中除了存储了虚拟和物理地址的映射之外,还存储着对应物理地址的权限位(比如r,w,rw等等)

        依靠这个权限,可以解释一个问题,那就是为什么c/c++一直说字符常量不可以被修改呢,那是因为字符常量也是要用虚拟地址转换成物理地址访问的,但字符常量地址对应的权限是只读的r。当试图修改的时候,就会被拦截报错了。同理,这也是为什么所谓的代码区(常量区)不能被修改

        注意,上面的字符常量,是不带const的,所谓的报错,也是运行时报错,编译器是不会报错的。但加了const,编译器就会有相应的检查,当我们对const修饰的变量修改的时候,编译器就会报错。之所以加const修饰,就是为了把运行时报错提前到编译阶段,以免错误被忽略。

        页表也存储着比如标识是否在内存(也是解决程序大小远远大于内存空间的情况),

写时拷贝具体是怎么实现的呢

        当子进程被创建后,数据段在页表中的权限会被修改为r,只读。当父子进程任一对某个数据进行写入,就会触发缺页中断,os会对中断进行判断,然后执行写时拷贝(拷贝该数据,父子进程的页表中该数据的权限被修改回rw,之后再完成相应的写入操作)

        注意,数据段所有的都会被改成r,就算触发了写时拷贝,那也会把要被写入的数据相应的权限改回rw。这样就可以实现按需写时拷贝,只要不写入,那就一直是只读状态。

        

1.3fork常规用法

一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如,父进程等待客户端请求,生成子进程来处理请求。

一个进程要执行一个不同的程序。例如子进程从fork返回后,调用exec函数。

1.4fork失败

第一种,就是系统有太多的进程了。

第二种,就是用户所能创建的进程是有限的。

2.进程终止

进程终止的本质是释放系统资源,就是释放进程申请的相关内核数据结构和对应的数据和代码。

2.1进程终止方法

正常终止(可以通过echo $?查看最近一次的进程退出码)

main返回

调用exit

_exit

异常退出

ctrl+c  信号终止。

2.2.1退出码

退出码(退出状态)可以告诉我们最后一次执行的命令的状态。在命令结束以后,我们可以知道命令是成功完成的还是以错误结束的。其基本思想是,程序返回退出代码0 时表示执行成功,没有问题。0以外的任何代码都被视为不成功。(main函数的返回值其实也是进程的退出码)
Linux Shell 中的主要退出码:

退出码解释
0命令成功执行
1通用错误代码
2命令(或参数)使用不当
126权限被拒绝(或)无法执行
127未找到命令,或PATH错误
128+n命令被信号从外部终止,或遇到致命错误
130通过Ctrl+C或SIGINT终止(终止代码2或键盘中断)
143通过SIGTERM终止(默认终止)
255/*退出码超过了0-255的范围,因此重新计算(LCTT译注:超过255后,用退出取模)

        退出码0表示命令执行无误,这是完成命令的理想状态。

        退出码 1我们也可以将其解释为“不被允许的操作”。例如在没有sudo权限的情况下使用yum;再例如除以0等操作也会返回错误码 1,对应的命令为 let a=1/0

        130(SIGINT或ctrl+c)和143(SIGTERM)等终止信号是非常典型的,它们属于128+n信号,其中 n代表终止码。

        退出码的描述,可以用语言和系统自带的方法比如c/c++的strerror函数获取,另一种就是自定义(一种就是手动打印错误描述,将自定义的各种错误放入函数,根据传入值返回不同字符串。最后return 传入值即可,这个不会改变strerror的内容)。

strerror(退出码)
返回字符串

像是我们平时用的指令,也是有退出码的。

2.2.2_exit终止进程

#include <unistd.h>
void _exit(int status);
参数:status 定义了进程的终止状态,父进程通过wait来获取该值

说明:虽然status是int,但是仅有低8位可以被父进程所用。所以_exit(-1)时,在终端执行$?发现
返回值是255。

注意,这个是系统调用的接口
在代码中的任意位置调用_exit,都表示进程退出

2.2.3exit函数终止进程

#include <stdlib.h>
void exit(int status);

exit最后也会调用_exit,但在调用_exit之前,还做了其他工作:

1. 执行用户通过atexit或on_exit定义的清理函数。

2. 关闭所有打开的流,所有的缓存数据均被写入

3. 调用_exit

int main()
{
printf("hello");
exit(0);
}
运⾏结果:
[root@localhost linux]# ./a.out hello[root@localhost linux]#int main()
{
printf("hello");
_exit(0);
}运⾏结果:
[root@localhost linux]# ./a.out
[root@localhost linux]#

在代码中的任意位置调用exit,都表示进程退出

这个是c标准库提供的库函数

这里再说句,这里刷新的缓冲区绝对不是os里面的(考虑效率的os在有写入的情况下,肯定要有刷新的,但_exit没有刷新缓冲区,说明不刷新缓冲区不会影响os内部),具体后面会有相应文章写下。

2.2.4return

        return是一种更常见的退出进程方法。执行return n等同于执行exit(n),因为调用main的运行时函数会将main的返回值当做exit的参数。

        其他函数return只是表示函数调用结束。只有main函数的return才是进程结束。

2.2.5errno

c/c++中,还有一个errno的变量,头文件是<errno.h>

        我们平时库函数调用之后,除了函数的return内容之外,库函数会有这样的规范,当函数执行情况是失败的时候(由设计者认为是失败的结果),会设置errno这个变量(全局的,函数调用结束后可以在主函数访问),这个跟退出码可以合称为错误码。

        通过strerror函数也可以获取这个错误编号的描述。

2.3进程退出场景

进程需要终止的情况
代码运行完毕,结果正确

代码运行完毕,结果不正确

代码异常终止

因此一个进程终止了,我们首先要判断的是 是否出异常了,然后才是关注进程的结果正不正确,不正确的话原因是什么。(因为如果进程出现了异常,进程的结果就没有意义了)

异常,各个语言都有相关的定义。

只是对于进程而言,异常就是收到了相应的信号。

在linux中像是kill -8就是给予除0异常,效果等同于除0的程序运行后的结果。

kill里的各个选项就是os的各种信号(信号以后会写文章介绍)。

其中数字是信号编号。字符是宏名称。我们可以kill -8 也可以kill -SIGFPE

每个信号都有不同编号,不同的信号编号意味着不同的异常原因

因此进程的最终执行情况,我们可以用2个数字来表示。一个是信号,一个是退出码。

3.进程等待

3.1进程等待必要性

之前讲过,子进程退出,父进程如果不管不顾,就可能造成‘僵尸进程’的问题,进而造成内存
泄漏。

另外,进程一旦变成僵尸状态,那就刀枪不入,“杀人不眨眼”的kill-9也无能为力,因为谁也没有办法杀死一个已经死去的进程。

最后,父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是
不对,或者是否正常退出。

父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息

3.2进程等待方法

3.2.1wait

#include<sys/types. h>
#include<sys/wait.h>pid_t wait(int* status);返回值:成功返回被等待进程pid,失败返回-1。参数:输出型参数,获取子进程退出状态,不关心则可以设置成为NULL

默认进行阻塞等待,即父进程会一直等,等到子进程结束了返回之后,再继续执行wait后面的语句。

等待任意一个子进程

3.2.2waitpid

pid_ t waitpid(pid_t pid, int *status, int options);
返回值:当正常返回的时候waitpid返回收集到的子进程的进程ID;如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0;如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;
参数:pid:Pid =- 1,等待任一个子进程。与wait等效。Pid>0.等待其进程ID与pid相等的子进程。status:输出型参数WIFEXITED(status):若为正常终止子进程返回的状态,则为真。(查看进程
是否是正常退出)WEXITSTATUS(status):若WIFEXITED非零,提取子进程退出码。(查看进程
的退出码)options:默认为0,表示阻塞等待WNOHANG:若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等
待。若正常结束,则返回该子进程的ID。

如果子进程已经退出,调用wait/waitpid时,wait/waitpid会立即返回,并且释放资源,获得子
进程退出信息。

如果在任意时刻调用wait/waitpid,子进程存在且正常运行,则进程可能阻塞。

如果不存在该子进程,则立即出错返回

3.2.3获取子进程status

wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充。

如果传递NULL,表示不关心子进程的退出状态信息。

否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。

status不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究status低16
比特位,图来自网络):

其中,高8位是退出码,低8位是core dump+信号码。核心就是我们前面说的“因此进程的最终执行情况,我们可以用2个数字来表示。一个是信号,一个是退出码。”先看信号,再看退出码

        我们可以用一个int变量来做这个输出型参数,像是我们exit(1),那么得到的就会是256。其中,得到信号码可以用(*status)&0x7F,因为0x7F是除低7位之外皆为0,与一下就可以把除低7位之外的1全部变0。同理退出码是((*status)>>8)&0xFF,右移8位,次8位变低8位,然后跟0xFF与一下,除低8位外的1皆变0。

        如果想测试信号提取的话,可以在程序运行时通过kill来给指定进程指定信号,这样就能测试了。

        c语言也提供了2个宏,不用我们手动进行位操作。

WIFEXITED(status):若为正常终止子进程返回的状态,则为真。(查看进程
是否是正常退出)
WEXITSTATUS(status):若WIFEXITED非零,提取子进程退出码。(查看进程
的退出码)if(WIFEXITED(status))
{printf("正常终止,获取退出码:%d",WEXITSTATUS(status));
}
else{printf("异常退出);
}

        之所以必须这样获取信息,而不是通过一些全局变量,是因为进程间不能直接交换数据,父子进程有写时拷贝,数据修改无法共享。所以必须通过这些系统调用来获取

        这些系统调用核心就是把task_struct中的exit_code和exit_singal这两个int变量拼成一个整数返回(子进程结束后,大部分资源释放,只保留pcb,直到被父进程接受)

        

3.2.4阻塞和非阻塞等待

        阻塞等待很好理解,就是父进程运行到wait/waitpid语句后如果子进程结束了,父进程会接受子进程的信息,子进程真正结束,父进程继续执行代码,不然的话,父进程会一直等,等到子进程结束了,再继续执行wait后面的语句。

        wait默认阻塞等待,waitpid是通过option参数来的。默认是0,阻塞等待;

        那么非阻塞等待的选项就是,WNOHANG:若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID。如果调用中出错(比如pid对应的子进程不存在),则返回-1,这时errno会被设置成相应的值以指示错误所在;

        WNOHANG是单次的非阻塞,每次都检查一次子进程状态马上返回,我们可以通过循坏的方式不停的检测,即轮询,中途也可以执行别的代码,像是执行一个函数,这个函数里会循坏遍历提前预设(把数组每个元素赋予不同函数,不同函数意味着不同的任务)的全局函数指针数组。而阻塞等待就是什么都做不了,一直等子进程。

        hang有悬挂之类的意思,可以理解为宕机,阻塞也可以理解为宕机,因此,wait no hang就是等待不宕机。

        

4.进程程序替换

fork()之后,父子各自执行父进程代码的一部分如果子进程就想执行一个全新的程序呢?进程的程序替换来完成这个功能!

程序替换是通过特定的接口,加载磁盘上的一个全新的程序(代码和数据),加载到调用进程的地址空间中!

像是shell,一些oj,一些搜索引擎都可以用到这个。

4.1原理

用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec 函数以执行另一个程序。当进程调用一种exec 函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec 并不创建新进程,所以调用exec 前后该进程的id 并未改变

如下为网图

        

        创建一个进程,是先创建pcb,地址空间,页表等,之后再选择性加载代码和数据(比如利用缺页中断,当真正访问物理内存的时候,再把相应代码和数据放入物理内存,再形成页表映射)。

        程序替换本质上其实就是在加载内存,所以如果是新进程,那就是创建相应的pcb等,再依靠程序替换的原理将程序加载到内存。

        而磁盘和内存都是硬件,代码和数据从磁盘到内存,要拷贝,要访问,只能由os来操作,所以os提供了程序替换的系统调用

        事实上,我们基本都是让子进程执行exec,这样就可以用父进程获得子进程的退出状态

        我们知道,父进程和子进程的代码和数据一开始全都是共享的,但程序替换需要覆盖代码和数据,所以这时候需要触发写时拷贝。注意,代码也会写时拷贝,机制跟数据是一样的,两者在子进程页表中最开始都是r权限的,所以当要覆盖代码和数据的时候,都会触发写时拷贝,这样就不会影响父进程了

        那么shell是如何执行指令的?shell自己就是一个一直运行的程序,shell会创建子进程,让子进程执行程序替换,替换的程序就是我们输入的指令(指令也是程序)

4.2替换函数

6种exec开头的函数(系统调用)

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 execve(const char *path, char *const argv[], char *const envp[]);
int execvpe(const char *file, char *const argv[], char *const envp[]);

这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回(原先程序代码数据都被覆盖,不会再往下执行)。

如果调用出错则返回 -1

所以exec函数只有出错的返回值而没有成功的返回值(所以exec函数之后直接写出错后要执行的代码即可)

l(list):表示参数采用列表v (vector):参数用数组p (path):有p 自动搜索环境变量
PATHe (env):表示自己维护环境变量...可变参数,传几个都行
path就是程序的路径

举例:

#include <unistd.h>int main()
{char *const argv[] = {"ps", "-ef", NULL};char *const envp[] = {"PATH=/bin:/usr/bin", "TERM=console", NULL};execl("/usr/bin/ps", "ps", "-ef", NULL);//第一个是程序的路径,这里是linux的ps指令的路径。//后面的参数,就是指令和选项,最后以null结尾即可。//类似的也可以这样execl("/usr/bin/top", "top", NULL);//还有其他非标准的传参,比如execl("/usr/bin/top", "/usr/bin/top", NULL);// 带p的,可以使用环境变量PATH,无需写全路径execlp("ps", "ps", "-ef", NULL);
d// 带e的,需要自己组装环境变量execle("ps", "ps", "-ef", NULL, envp);execv("/usr/bin/ps", argv);// 带p的,可以使用环境变量PATH,无需写全路径execvp("ps", argv);// 带e的,需要自己组装环境变量execve("/usr/bin/ps", argv, envp);exit(0);
}

        我们思考一下,为什么excel我们没有手动传环境变量,但新程序中我们也可以获取到环境变量?因为不需要手动。要知道命令行参数和环境变量在进程地址空间是单独存放(与代码和数据)的,而子进程是会继承大部分父进程的进程地址空间的,所以父子进程获取到的环境变量,在未中途修改的情况下,是通过页表(映射关系也会被继承)来指向同一片存储环境变量的物理内存地址,就算修改了,触发下写时拷贝,改下页表映射,写入一下就行了。

        最重要的是,进程替换,是不会替换环境变量数据的,也就是说上面的机制是不会被进程替换所破坏

        还要注意的是,子进程继承环境变量后,可以在自己进程(或者说代码)中新增环境变量,这个操作只会影响该进程所创建的子进程,不会影响他的父进程。

        上面不管是继承,还是继承之后新增,都是在原有基础上的。而excele这种末尾带e,其实就是手动传环境变量(新程序的环境变量只会有你传的,是覆盖的)。当然,我们可以自定义一个环境变量表传进去,也可以利用environ获取到的当前进程环境变量直接传。

        

.cc .cpp .cxx都是linux下c++的后缀。

        在makefile中我们怎么同时生成两个可执行程序呢?一般来说,makefile只会以第一个生成为目标。

        如下即可生成testcpp和testc两个可执行,all依赖这2个可执行,但没有依赖方法,不做任何事,但因为依赖,所以这2个可执行必须先生成,从而达成目的。

.PHONY:all
all:testcpp testctestcpp:testcpp.ccg++ -o $@ $^ -std=c++11
testc:testc.cgcc -o $@ $^.PHONY:clean
clean:rm -f testcpp testc

那么是否可以exec调用自己的程序呢?肯定可以的,而且是不限格式的,比如说我父进程是c语言,子进程可以是c++,python,shell脚本等等,只要是能在linux下运行的(这些语言运行后,都是进程,都有对应的代码和数据,所以在os眼里都一样)。

testc.c下:excel("./testcpp","testcpp","-a","-2",NULL);也可以调shell脚本
excel("/usr/bin/bash","bash","test.sh",NULL);python
excel("/usr/bin/python3","python3","test.py",NULL);

        我们仔细看看就会发现,这些exec,在功能上没区别,只是在使用上有区别,以应对不同的使用场景。 

        事实上,只有execve 是真正的系统调用,其它五个函数最终都调用execve,所以 execve在 man手册 第2节,其它函数在 man手册第3节。这些函数之间的关系如下图(网图)所示。

5.自定义shell

初版

#include<stdio.h>
#include<stdlib.h>
#include<string.h>  
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>#define SIZE 1024       //命令最大长度
#define MAX_ARGV 64     //命令可以分割的最大长度
#define SEP " "         //命令分割的依据
char *argv[MAX_ARGV];   //用于存放切割后的命令//获取环境变量中需要的内容,比如主机名、用户名、当前工作目录
const char* GetEnv(const char *str){char *EnvValue=getenv(str);if(EnvValue){//有获取到return EnvValue;}else{return "None";}
}//把交互部分封装起来
void Interactive(char *out,int size){//输出提示符printf("[%s@%s %s]$ ",GetEnv("USER"),GetEnv("HOSTNAME"),GetEnv("PWD"));//获取命令字符串,如:ls -a -d,获取一行,fgets会自动加\0,也会读到回车fgets(out,size,stdin);//把回车去掉,另外fgets可以保证最少会收到一个回车符,不会是空串,可以放心访问out[strlen(out)-1]='\0';
}void Split(char in[]){int i=0;argv[i++]=strtok(in,SEP);//对历史字符串,其余字符要传NULL,另外最后一定要赋予NULL,该数组是用于exec的while(argv[i++]=strtok(NULL,SEP));//当不能再切了之后,就会返回NULL,NULL赋值之后,while跳出
}void Execute(){pid_t id=fork();if(id==0){//子进程执行命令execvp(argv[0],argv);    exit(0);}pid_t rid=waitpid(id,NULL,0);
}int main(){while(1){//交互:输出提示符,获取命令字符串char commandline[SIZE];Interactive(commandline,SIZE);//对命令字符串进行切割Split(commandline);//执行命令Execute();}return 0; 
}

但还是有一些问题,比如cd的命令,我们是直接让子进程执行的,这意味着只是子进程切换了目录,但父进程,也就是我们自己写的bash没有切换目录。

小细节,因为编码问题,退格键可能会直接以编码形式输入,而不是实现删除,需要ctrl+退格back键

  基础版(有兴趣可以自己继续加内容):

#include<stdio.h>
#include<stdlib.h>
#include<string.h>  
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>#define SIZE 1024       //命令最大长度
#define MAX_ARGV 64     //命令可以分割的最大长度
#define SEP " "         //命令分割的依据
char *argv[MAX_ARGV];   //用于存放切割后的命令
char pwd[SIZE];  //用于存储当前路径
char env[SIZE];   //用于导入环境变量
int lastcode=0;    //存储最近一次子进程的退出码//获取环境变量中需要的内容,比如主机名、用户名、当前工作目录
const char* GetEnv(const char *str){char *EnvValue=getenv(str);if(EnvValue){//有获取到return EnvValue;}else{return "None";}
}//把交互部分封装起来
int Interactive(char *out,int size){//输出提示符printf("[%s@%s %s]$ ",GetEnv("USER"),GetEnv("HOSTNAME"),GetEnv("PWD"));//获取命令字符串,如:ls -a -d,获取一行,fgets会自动加\0,也会读到回车fgets(out,size,stdin);//把回车去掉,另外fgets可以保证最少会收到一个回车符,不会是空串,可以放心访问out[strlen(out)-1]='\0';return strlen(out);
}void Split(char in[]){int i=0;argv[i++]=strtok(in,SEP);//对历史字符串,其余字符要传NULL,另外最后一定要赋予NULL,该数组是用于exec的while(argv[i++]=strtok(NULL,SEP));//当不能再切了之后,就会返回NULL,NULL赋值之后,while跳出if(strcmp(argv[0],"ls")==0){argv[i-1]=(char*)"--color=auto";argv[i]=NULL;}//linux的shell,是把ls作为ls --color=auto的别名了。
}void Execute(){pid_t id=fork();if(id==0){//子进程执行命令execvp(argv[0],argv);    exit(0);}int status=0;pid_t rid=waitpid(id,&status,0);if(rid==id)lastcode=WEXITSTATUS(status);}int BuildinCmd(){int ret=0;//如果是内键命令,返回1,否则0if(strcmp("cd",argv[0])==0){ret=1;char * target=argv[1]; //cd xxx or cd //同样对应target要么是个路径,要么就是个NULL,if(!target)target=(char*)GetEnv("HOME");chdir(target);//改变工作目录char tmp[1024];getcwd(tmp,1024);snprintf(pwd,SIZE,"PWD=%s",tmp);//sn就是把printf的内容传入n大小的字符串里,s就是不限制大小。//这里就是SIZE大小的pwd字符串。putenv(pwd);//chdir不会更改环境变量,需要我们手动更改}else if(strcmp("export",argv[0])==0){ret=1;//if(argv[1])putenv(argv[1]); 不能这样写,因为argv[1]存的是指针//,指向的commandline,而commandline每次循环都会被新的命令字符串覆盖,导致原先更改的环境变量内容被覆盖if(argv[1]){strcpy(env,argv[1]);putenv(env);}//这样写其实也有问题的,我env是一维数组,//每次都是维护一个环境变量,只要我多次export,旧的环境变量就没了//,所以,其实是要维护一整个环境变量表的,但这里为了方便,没写//这也是export的底层,就是更改shell维护的环境变量表}else if(strcmp("echo",argv[0])==0){ret=1;if(argv[1]==NULL){printf("\n");}else {if(argv[1][0]=='$'){if(argv[1][1]=='?'){printf("%d\n",lastcode);lastcode=0;}else{char *e=getenv(&argv[1][1]);if(e)printf("%s\n",e);}}else{printf("%s\n",argv[1]);}}}return ret;
}int main(){while(1){//交互:输出提示符,获取命令字符串char commandline[SIZE];int n=Interactive(commandline,SIZE);if(n==0)continue;//对命令字符串进行切割Split(commandline);//处理内键命令,如cd等n=BuildinCmd();if(n)continue;//执行命令Execute();}return 0; 
}

        其实从这里export就可以看出,之所以我们在shell中更改环境变量,出错了只需要重新启动shell,就是因为我们做的都是对shell进程从配置文件中继承下来的环境变量表修改,出错重新启动shell,重新读取配置文件即可。

        同样,之所以子进程可以获得父进程(有对环境变量做修改)的环境变量,就是因为fork之后,环境变量也是继承下来的。

http://www.dtcms.com/a/467298.html

相关文章:

  • 电脑网站微信支付怎么做的企业建设网站公司有哪些
  • 欧美做电影 迅雷下载网站网站工商备案查询
  • 深圳办公室装修哪家好正规的网站优化推广公司
  • 网站硬件方案全椒有做网站的吗
  • 提供做网站公司移动开发软件
  • 南阳卧龙区网站建设哪家好wordpress页面权限设置
  • 做网站win7好用么广州建网站比较有名的公司
  • 我的全栈学习之旅:FastAPI (持续更新!!!)
  • 做电影网站要怎么拿到版权资源下载网站建设
  • 佛山找企业的网站苏州实力做网站公司有哪些
  • 网站技术策划人员要求怎么搞到网站
  • 在 tinkinter 中 label 标签的 font 属性有哪几种形式设置?
  • wordpress友情链接激活seo关键词
  • 晋城网站制作公司怎么选wordpress 最新一片文章
  • 淄博市沂源县城乡建设局网站wordpress 删除 wordpress.org
  • 阿里云怎么放多个网站wordpress微信拦截
  • 餐饮营销型网站案例分析域名注册查询阿里云
  • 单位网站怎么做2021年uc秒懂网址
  • 高端个性化网站开发室内设计学校排行榜
  • 上海建设银行网站市场营销策略分析论文
  • Linux学习笔记--POLL_SELECT方式读取输入数据
  • STM32项目分享:基于智能电扇的设计与实现
  • 大兴智能网站建设哪家好陕西宁德建设工程有限公司网站
  • 没有网站可以域名备案吗wordpress安装后只有英文
  • 网站和网页的设计方法南翔做网站公司
  • 外贸网络推广价格seo值是什么意思
  • 简单商业网站模板找网站公司制作网站
  • Unity Mask镂空效果(常用于新手引导或高亮显示UI元素)
  • 网站名是什么免费视频网站大全
  • Unity实现圆柱螺旋曲线运动方程