首先,在百度百科中,环境变量的解释是这样的:
环境变量(environment variables)一般是指在操作系统中用来指定操作系统运行环境的一些参数,如:临时文件夹位置和系统文件夹位置等。环境变量是在操作系统中一个具有特定名字的对象,它包含了一个或者多个应用程序所将使用到的信息。例如Windows和DOS操作系统中的path环境变量,当要求系统运行一个程序而没有告诉它程序所在的完整路径时,系统除了在当前目录下面寻找此程序外,还应到path中指定的路径去找。用户通过设置环境变量,来更好的运行进程。
指令本质上就是编译好的程序和脚本,被存储在特定的路径下(默认/user/bin/)。
比如我们执行 ls 指令,实际上就是执行这个程序或者脚本,而我们要执行一个程序就必须找到该程序,我们通常要运行一个可执行程序,是用 ./a.out
来执行的,./
表示在当前目录,a.out
表示一个可执行程序。所以我们要执行 ls 指令,就要找到 ls 所在的路径,而环境变量的作用就是让系统从指定的路径去找,而在PATH 中有/uer/bin
路径,所以我们就能执行所有的指令了。
常用的10个环境变量如下:
环境变量名称 | 作用 |
---|---|
HOME | 用户的主目录(也称家目录) |
SHELL | 用户使用的 Shell 解释器名称 |
PATH | 定义命令行解释器搜索用户执行命令的路径 |
EDITOR | 用户默认的文本解释器 |
RANDOM | 生成一个随机数字 |
LANG | 系统语言、语系名称 |
HISTSIZE | 输出的历史命令记录条数 |
HISTFILESIZE | 保存的历史命令记录条数 |
PS1 | Bash解释器的提示符 |
邮件保存路径 |
Linux 作为一个多用户多任务的操作系统,能够为每个用户提供独立的、合适的工作运行环境,因此,一个相同的环境变量会因为用户身份的不同而具有不同的值。
按照变量的生存周期划分,Linux 变量可分为两类:
- 永久的:需要修改配置文件,变量永久生效。
- 临时的:使用 export 命令声明即可,变量在关闭 shell 时失效。
按作用的范围分,在 Linux 中的变量,可以分为环境变量和本地变量:
- 环境变量:相当于全局变量,存在于所有的 Shell 中,具有继承性;
- 本地变量:相当于局部变量只存在当前 Shell 中,本地变量包含环境变量,非环境变量不具有继承性。
值得一提的是,Linux 系统中环境变量的名称一般都是大写的,这是一种约定俗成的规范。
使用 echo 命令查看单个环境变量,例如:echo $PATH
;使用 env 查看当前系统定义的所有环境变量;使用 set 查看所有本地定义的环境变量。查看 PATH 环境的实例如下:
常用的命令如下:
- echo: 显示某个环境变量值
- export: 设置一个新的环境变量
- env: 显示所有环境变量
- unset: 清除环境变量
- set: 显示本地定义的shell变量和环境变量
例如我们要设置一个新的环境变量,然后再清除:
`
在 Linux 中设置环境变量有三种方法:
1.所有用户永久添加环境变量: vi /etc/profile
,在 /etc/profile
文件中添加变量。
2.当前用户永久添加环境变量: vi ~/.bashrc
,在用户目录下的 ~/.bashrc
文件中添加变量。
3.临时添加环境变量 : 可通过 export 命令,如运行命令export HELLO=100
。
#include
int main(int argc, char *argv[], char *env[])
{
int i = 0;
for(; env[i]; i++){
printf("%s\n", env[i]);
}
return 0;
}
#include
int main(int argc, char *argv[])
{
extern char **environ;
int i = 0;
for(; environ[i]; i++){
printf("%s\n", environ[i]);
}
return 0;
}
lib.c中定义的全局变量environ指向环境变量表,environ没有包含在任何头文件中,所以在使用时要用extern声明。
#include
#include
int main()
{
printf("%s\n", getenv("PATH"));
return 0;
}
正常退出:
- 从 main 函数返回
- 调用exit
- 调用_exit
异常退出:
- crtl + c,信号终止
_exit():
#include
void _exit(int status);
DESCRIPTION:
The function _exit() terminates the calling process “immediately”. Any open file descriptors belonging to the process are closed; any children of the process are inherited byprocess 1, init, and the process’s parent is sent a SIGCHLD signal.
_eixt()函数立即终止进程,关闭所有属于该进程的文件描述符,其子进程被1号init 进程领养,然后向父进程发送SIGCHLD 信号。
The value status is returned to the parent process as the process’s exit status, and can be collected using one of the wait(2) family of calls.
值状态作为进程的退出状态返回到父进程,并且可以使用 wait(2) 系列调用之一来收集。
exit():
#include
void exit(int status);
- 执行用户通过 atexit或on_exit定义的清理函数。
- 关闭所有打开的流,所有的缓存数据均被写入
- 调用_exit
return是一种更常见的退出进程方法。执行return n等同于执行exit(n),因为调用main的运行时函数会将main的返回值当做 exit的参数。
进程等待的必要性:
- 子进程退出,父进程如果不管不顾,就可能造成‘僵尸进程’的问题,进而造成内存泄漏。
- 进程一旦变成僵尸状态,那就刀枪不入,“杀人不眨眼”的kill -9 也无能为力,因为谁也没有办法杀死一个已经死去的进程。
- 父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是不对,或者是否正常退出。
- 父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息
wait():用来等待任何一个子进程退出,由父进程调用。
#include
#include
pid_t wait(int*status);
返回值:
成功返回被等待进程pid,失败返回-1。
参数:
输出型参数,获取子进程退出状态,不关心则可以设置成为NULL
wait方式:
阻塞式等待,等待的子进程不退出时,父进程一直不退出;
waitpid():
pid_ t waitpid(pid_t pid, int *status, int options);
参数:
pid:
Pid=-1,等待任一个子进程。与wait等效。
Pid>0.等待其进程ID与pid相等的子进程。
status:
WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
options:
当options参数为0时,与wait功能相同,仍是阻塞式等待,不提供额外功能
如果为下列常量按位或则提供更多功能:
WCONTINUED:若实现支持作业控制,那么由pid指定的任一子进程在暂停后已经继续,但状态尚未报告,则返回状态
WNOHANG:若由pid指定的子进程并不是立即结束,则waitpid不阻塞,即此时以非阻塞方式(轮询式访问的必要条件)等待子进程,并且返回0。若正常结束,则返回该⼦进程的ID。
WUNTRACED:若实现支持作业控制,而pid指定的任一子进程已经暂停,且其状态尚未报告,则返回其状态。
返回值:
当正常返回的时候,waitpid返回收集到的子进程的进程ID;
如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0;
如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;
当pid所指示的子进程不存在,或此进程存在,但不是调用进程的子进程,waitpid就会出错返回,这时errno被设置为ECHILD;
获取子进程status:
我们可以通过status & 0x7f来判断异常信号是否为0;若为0,则正常退出,然后可以通过(status >> 8) & 0xff来获取子进程返回值。sys/wait.h中提供了一些宏来简化这些操作:
if (WIFEXITED(status)) {
// 正常退出:((status) & 0x7f) == 0
// 打印退出码:(status >> 8) & 0xff
printf("child return: %d\n", WEXITSTATUS(status));
} else if (WIFSIGNALED(status)) {
// 异常退出:((signed char) (((status) & 0x7f) + 1) >> 1) > 0
// 打印异常信号值:(status) & 0x7f
printf("child signal: %d\n", WTERMSIG(status));
}
我们用这段代码来读取子进程的status,以获取它的退出码和异常信号值,以及父进程收到的SIGCHLD信号:
#include
#include
#include
#include
#include
void catchsig(int sig)
{
printf("catch a sig : %d , pid->%d\n",sig,getpid());
}
int main()
{
signal(SIGCHLD,catchsig);
pid_t id = fork();
if(id<0)
{
printf("fork error!\n");
exit(-1);
}
else if(id==0)
{
int cnt = 3;
while(cnt)
{
printf("this is a child process! id-->%d pid-->%d ppid--> %d\n",cnt--,getpid(),getppid());
}
//exit(-1);
int i = 10/0;
}
sleep(2);
int status = 0;
pid_t ret = waitpid(id,&status,0);
if(id>0)
//0-7:终止信号 15-8:退出状态
printf("wait success:%d , sign number:%d , child exit code:%d\n",ret,(status & 0x7F),(status>>8 & 0xFF));
return 0;
}
运行结果如下:
其实,⼦进程在终⽌时会给⽗进程发SIGCHLD信号,该信号的默认处理动作是忽略,⽗进程可以⾃定义SIGCHLD信号的处理函数,这样⽗进程只需专⼼处理⾃⼰的⼯作,不必关⼼⼦进程了,⼦进程终⽌时会通知⽗进程,⽗进程在信号处理函数中调⽤wait清理⼦进程即可。一般情况下父进程收到这个信号的默认处理是忽略这个信号,即就是不做任何处理。
替换原理:
用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。
替换函数:
#include
extern char **environ;
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[], *const envp[]);
这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回。
如果调用出错则返回-1,所以exec函数只有出错的返回值而没有成功的返回值。
命名理解:
l(list) : 表示参数采用列表
v(vector) : 参数用数组
p(path) : 有p自动搜索环境变量PATH
e(env) : 表示自己维护环境变量
int execl(const char *path, const char *arg, ...);
其中*path表示的是路径,arg表示的是要执行的程序,“…”表示的就是可变参数列表,即命令行上怎么执行这里就写入什么参数。必须以NULL作为参数列表的结束。
#include
#include
#include
#include
int main()
{
if(fork()==0)
{
printf("command begin\n");
execl("/usr/bin/ls","ls","-a","-l",NULL);
printf("command fail\n");
exit(1);
}
waitpid(-1,NULL,0);
printf("wait child success\n");
return 0;
}
当子进程执行完打印command begin的语句的时候,进行进程的替换。其中替换的是/usr/bin/ls,在命令行要输入的是ls -a -l,将程序运行起来:
下面是这些函数的用法:
#include
int main()
{
char *const argv[] = {"ps", "-ef", NULL};
char *const envp[] = {"PATH=/bin:/usr/bin", "TERM=console", NULL};
execl("/bin/ps", "ps", "-ef", NULL);
// 带p的,可以使用环境变量PATH,无需写全路径
execlp("ps", "ps", "-ef", NULL);
// 带e的,需要自己组装环境变量
execle("ps", "ps", "-ef", NULL, envp);
execv("/bin/ps", argv);
// 带p的,可以使用环境变量PATH,无需写全路径
execvp("ps", argv);
// 带e的,需要自己组装环境变量
execve("/bin/ps", argv, envp);
exit(0);
}
操作系统实际上只提供了一个接口那就是:execve,其他的函数都是对该接口封装而成的库函数。它们的底层都是使用execve来进行实现的。
以下代码利用程序替换实现了shell 的基本功能,包括重定向,退出码等功能,后续还可以再补充。
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define NUM 1024//缓冲区大小
#define COM_NUM 64//存放指令字符串的最大值
#define NONE_REDIR 0
#define INPUT_REDIR 1
#define OUPUT_REDIR 2
#define APPEND_REDIR 4
char lineCommand[NUM];//输入缓冲区
char *myargv[COM_NUM];//存放一个个的指令字符串
int exit_code=0;//退出码
int exit_sign=0;//退出信号值
int redirType = NONE_REDIR;//读方式
char* myFile = NULL;
//跳过空格
#define trimSpace(start) do{while(isspace(*start)) ++start;}while(0)
void commandCheck(char* commands)
{
assert(commands);
char *start = commands;
char *end = commands+strlen(commands);
while(start<end)
{
if(*start == '>')
{
*start = '\0';
start++;
if(*start == '>') // >> 表示追加重定向
{
start++;
redirType = APPEND_REDIR;
}
else // > 表示输出重定向
{
redirType = OUPUT_REDIR;
}
trimSpace(start);
myFile = start;
break;
}
else if(*start == '<') // < 表示输入重定向
{
*start = '\0';
start++;
redirType = INPUT_REDIR;
trimSpace(start);
myFile = start;
break;
}
else
start++;
}
}
//最新添加重定向功能!!!!!
int main()
{
while(1)
{
redirType = NONE_REDIR;
myFile = NULL;
printf("[wml @ my_bash path#]");
fflush(stdout);
//获取输入行内容
char *s = fgets(lineCommand,sizeof(lineCommand)-1,stdin);
assert(s != NULL);
(void)s;
lineCommand[strlen(lineCommand)-1] = 0;
//重定向:
//"ls -a -l > "test.txt"" ----> "ls -a -l" > "test.txt"
//"ls -a -l >> "test.txt"" ----> "ls -a -l" >> "test.txt"
//"cat < "test.txt"" ----> "cat" < "test.txt"
commandCheck(lineCommand);
//切割字符串
myargv[0] = strtok(lineCommand," ");
int i = 1;
//添加颜色选项
if(myargv[0]!= NULL && strcmp(myargv[0],"ls") == 0 )
{
myargv[i++] = (char*) "--color=auto";
}
//读取每个选项
while(myargv[i++] = strtok(NULL," "));
//解决工作路径无法改变的问题
if(myargv[0]!=NULL && strcmp(myargv[0],"cd") == 0)
{
if(myargv[1]!=NULL)
chdir(myargv[1]);
continue;
}
//设置退出码
if(myargv[0]!=NULL && myargv[1]!=NULL && strcmp(myargv[0],"echo")==0 )
{
if(strcmp(myargv[1],"$?")==0)
printf("exit_code-->%d | exit_sign-->%d\n",exit_code,exit_sign);
else
printf("%s\n",myargv[1]);
continue;
}
#ifdef DEBUG
for(int i=0;myargv[i];i++)
printf("myargv[%d]:%s\n",i,myargv[i]);
#endif
pid_t id = fork();
assert(id!=-1);
if(id==0)
{
switch(redirType)
{
case NONE_REDIR:
break;
case INPUT_REDIR:
{
int fd = open(myFile,O_RDONLY);
if(fd<0) {perror("open");return 1;}
dup2(fd,0);
}
break;
case OUPUT_REDIR:
case APPEND_REDIR:
{
umask(0);
int flag = O_WRONLY | O_CREAT;
if(redirType == APPEND_REDIR) flag |= O_APPEND;
else flag |= O_TRUNC;
int fd = open(myFile,flag,0666);
if(fd<0){perror("open");return 2;};
dup2(fd,1);
}
break;
default:
printf("bug!\n");
break;
}
execvp(myargv[0],myargv);
_exit(1);
}
int status=0;
pid_t ret = waitpid(id,&status,0);
assert(ret!=-1);
(void)ret;
exit_sign = (status & 0x7f);
exit_code = (status>>8) & 0xff;
}
return 0;
}