本篇博客记录了Linux下一些概念的解释,以及环境变量的相关操作

所用系统:CentOS 7.6

1.概念解释

1.1 进程竞争性

系统进程的数目较多。而CPU资源等其他资源不够用,所以进程之间存在竞争性,也就出现了优先级,这在上篇博客中有过介绍

1.2 进程独立性

进程运行具有独立性,不会因为某个进程出错,而影响其他进程的运行

我们知道,一个进程是内核结构task_truck+代码和数据组成的。而linux系统是通过进程地址空间方式来保证进程的独立性,那是下篇博客会讲到的内容

1.3 并行

并行:多个进程在多个CPU下分割,同时运行

image-20221003184423298

我们一般的电脑都是只有1个cpu,那是怎么做到多个进程运行的?

注意:多个进程都在系统中运行≠多个进程在系统中同时运行。要想知道这是怎么做到的,需要了解并发的概念

1.4 并发

大部分操作系统都是分时的,操作系统会给每一个进程赋予一个时间片,这样在一个调度周期中,可以调用到每一个需要运行的进程。

这样,在一个时间段内,多个进程会通过交叉运行的方式,让每一个进程的代码,在这段时间内都能得到运行

比如每一个进程运行10ms,假设有10个进程需要运行,那么在1s内,这10个进程都会被运行10次。1s=1000ms

cpu进行多个进程的快速交替运行,以实现我们看到的单cpu运行多个进程的情况

这种情况就叫做并发


1.5 进程优先级管理

操作系统正在运行一个低优先级进程的时候,突然来了一个高优先级数据怎么办?

  • 操作系统支持不同优先级进程的存在
  • 同一个优先级的进程可以存在多个
  • 操作系统是用队列来管理进程的,队列只支持支持尾删尾插

这种情况要怎么处理呢?如果给队列头插,那不就破坏了队列的属性了?

实际上,操作系统并不是用单一队列来进行管理的,而是用了一个哈希桶来进行处理

image-20221003185922659

可以理解为哈希桶是一个数组,后面链接了不同优先级的进程

当一个更高优先级的进程需要运行的时候,操作系统只需要改变cpu当前运行队列在哈希桶里面的指向,即可马上完成高低优先级进程之间的切换

操作系统还会管理一个位图,用于标识某一个优先级内是否存在进程

哈希和位图这两个数据结构都会在C++的博客中讲解

除了位图和哈希表之外,操作系统还会管理一个结构体,用于标识目前正在使用的进程哈希桶active,以及新进程的管理桶old

1
2
3
4
struct runqueue{
hash_queue* active;
hash_queue* old;
};

当出现新进程的时候,操作系统不会把它直接链接到active里面,而是放入old中;

active内部的进程运行完了之后,只需要swap一下active/old指针,即可完成新进程表和运行完的旧表之间的交换!

  • 这种调度算法也被称为大O(1)调度法

1.6 进程间切换

CPU存在寄存器,这些寄存器储存了进程的临时数据

  • 寄存器分为可见寄存器(eax/ebx)和不可见寄存器

当进程在运行过程中,会产生各种临时数据

1
2
3
4
5
6
7
8
9
int test(){
int a = 10;
return a;
}
int main()
{
int b = test();
return 0;
}

我们知道,定义在函数test中的a是一个局部变量,出了作用域就会销毁,那么main函数里面的b是怎么拿到test的返回值的?

这里就用到了寄存器:a的值会先放入寄存器中,销毁了之后,再把寄存器里面的值赋值给b

和内存一样,CPU只有一套寄存器。这一套寄存器在运行不同进程的时候,可以保存不同的临时数据

这时候如果来了一个高优先级进程A,直接覆盖了正在运行的进程B及其寄存器中的数据,就可能导致进程B存在寄存器中的数据丢失,导致B无法继续运行

所以,在进程的task_struct结构体中,就有一个专门的成员用于保存进程的上下文数据

  • 上下文数据:进程在运行中产生的各种临时数据
  • 当进程被剥离的时候,需要保存上下文数据
  • 当进程恢复运行的时候,需要重新加载上下文数据

image-20221003191349705

关于进程pcb和task_struct的内容可以看我之前的博客 进程概念


2.环境变量

2.1 引入环境变量

当我们运行自己编译的一个可执行文件的时候,需要带上./指定路径

image-20221004161504688

使用file命令查看系统的相关指令,你会发现它们和我们自己写的mytest本质上是一样的,都是一个executable的可执行文件

1
2
3
4
5
6
[muxue@bt-7274:~/git/raspi/code/22-10-04_环境变量]$ file /bin/ls
/bin/ls: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.32, BuildID[sha1]=c8ada1f7095f6b2bb7ddc848e088c2d615c3743e, stripped
[muxue@bt-7274:~/git/raspi/code/22-10-04_环境变量]$ file /bin/gcc
/bin/gcc: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.32, BuildID[sha1]=84ea48c51fa70f8cd586b7801bc655487156db7b, stripped
[muxue@bt-7274:~/git/raspi/code/22-10-04_环境变量]$ file mytest
mytest: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.32, BuildID[sha1]=db352023d208d9f48899641c3536a8c13b7bc7bf, not stripped

那为何运行诸如ls pwd gcc等等系统命令的时候,不需要在前面带上./路径来运行呢?

1
2
3
4
[muxue@bt-7274:~/git/raspi/code/22-10-04_环境变量]$ mytest
-bash: mytest: command not found
[muxue@bt-7274:~/git/raspi/code/22-10-04_环境变量]$ ls
makefile mytest mytest.c

这是因为:指向一个可执行程序,前提是需要找到它!

linux系统只能找到它自己预设好的命令,找不到我们的mytest

在linux命令行中,输入env即可查看当前系统的环境变量

image-20221004162138864

其中PATH就是可执行程序存放的路径!系统就是通过环境变量来查找可执行程序的

2.2 添加删除环境变量

别急,我们先来学习一下怎么添加环境变量。实际上,我们的bash命令行里面是可以定义变量的,变量分为两种类型

  • 本地变量(局部)
  • 环境变量(全局)

直接使用变量名=值的方式,就可以定义一个本地变量。使用echo命令可以查看这个本地变量。这时候我们用env | grep 变量名在环境变量里面查找,会发现当前的环境变量里面没有这个东西

1
2
3
4
5
[muxue@bt-7274:~]$ aaaa=1234
[muxue@bt-7274:~]$ echo $aaaa
1234
[muxue@bt-7274:~]$ env | grep aaaa
[muxue@bt-7274:~]$

这时候需要用export命令,创建一个环境变量

1
2
3
[muxue@bt-7274:~]$ export bbbb=4321
[muxue@bt-7274:~]$ env | grep bbbb
bbbb=4321

或者可以导入当前的本地变量

1
2
3
[muxue@bt-7274:~]$ export aaaa
[muxue@bt-7274:~]$ env | grep aaaa
aaaa=1234

删除的时候则使用unset命令取消环境变量

1
2
3
[muxue@bt-7274:~]$ unset bbbb
[muxue@bt-7274:~]$ env | grep bbbb
[muxue@bt-7274:~]$

查看环境变量

  1. echo: 显示某个环境变量值
  2. export: 设置一个新的环境变量
  3. env: 显示所有环境变量
  4. unset: 清除环境变量
  5. set: 显示本地定义的shell变量和环境变量
1
2
echo $环境变量名 #查看环境变量
set | less #查看所有的shell变量和环境变量

认识一些环境变量

  • USER:当前登录的用户
  • HOME:当前用户的工作路径
  • LANG:当前的语言和编码设定
  • PATH:可执行命令的路径
  • SHELL:当前使用的命令行是啥
  • LOGNAME:当前登录的用户名
  • PWD:当前所处路径
  • OLDPWD:上一个路径,使用cd -跳回上一个路径
  • HISTSIZE:系统会记录的历史命令条数

我们可以用history命令查看之前运行过的命令,这里面保存的正好是3000条,和环境变量HISTSIZE的设置一致!

1
2
3
4
[muxue@bt-7274:~]$ env | grep $HISTSIZE
HISTSIZE=3000
[muxue@bt-7274:~]$ history | wc --l
3000

需要注意的是,系统预载的环境变量都是在配置文件里面的。当前我们对环境变量做的任何操作都只会临时保存。关闭当前的命令行重新开一个,之前设置的环境变量就会消失

1
[muxue@bt-7274:~]$ cat /etc/bashrc

系统的环境变量配置文件为/etc/bashrc,用户的则为工作目录下的.bashrc以及.bash_profile

查看上个进程退出状态码

在linux下有一个特殊的环境变量?,这个环境变量存放的是上一个进程的退出码。比如我们进程中return 0退出,那么查询到的就是0

1
echo $?

2.3 环境变量PATH

使用echo $PATH查看当前系统可执行文件的路径

这里的路径都以:作为分割,linux查找命令的时候,就会在下面的这些路径里面查找

1
2
[muxue@bt-7274:~]$ echo $PATH
/usr/local/bin:/usr/bin:/usr/local/sbin:/usr/sbin:/usr/local/python3/bin:/usr/local/python3/bin:/home/muxue/.local/bin:/home/muxue/bin

除了直接使用ls,我们也可以使用路径的方式来调用ls

1
2
[muxue@bt-7274:~]$ /usr/bin/ls 
bin git install.sh kook mon

而如果想让系统能找到自己的可执行程序,就可以直接把可执行程序复制到这些路径中!

给PATH中添加可执行文件

1
2
3
4
[muxue@bt-7274:~/git/raspi/code/22-10-04_环境变量]$ cp mytest ~/bin
[muxue@bt-7274:~/git/raspi/code/22-10-04_环境变量]$ mytest
hello world!
hello world!

比如现在,我把mytest这个可执行程序复制到了~/bin也就是/home/muxue/bin的路径下,此时直接使用mytest就能找到对应的命令了!

除了这种办法以外,我们还可以把当前的路径写入PATH环境变量中

1
2
3
4
[muxue@bt-7274:~]$ export PATH=$PATH:/home/muxue/git/raspi/code/test
#这样写是在原本的path后面追加内容。不能直接path=自己的路径,会直接覆盖
[muxue@bt-7274:~]$ echo $PATH
/usr/local/bin:/usr/bin:/usr/local/sbin:/usr/sbin:/usr/local/python3/bin:/usr/local/python3/bin:/home/muxue/.local/bin:/home/muxue/bin:/home/muxue/git/raspi/code/test

这时候直接执行mytest也成功了!

1
2
3
4
[muxue@bt-7274:~]$ mytest
hello world!
hello world!
hello world!

前面提到了我们设置的这个环境变量都是临时的,所以重启了之后,自己设置的这个路径也会消失

1
2
[muxue@bt-7274:~]$ echo $PATH
/usr/local/bin:/usr/bin:/usr/local/sbin:/usr/sbin:/usr/local/python3/bin:/usr/local/python3/bin:/home/muxue/.local/bin:/home/muxue/bin

注意:一般情况下不建议在linux系统路径中安装自己的可执行程序,因为这样会污染系统的命令环境!

3.C/C++获取环境变量

3.1 main函数的参数

之前一直没有了解过这个知识点,C/C++的main函数是可以带参数的!

1
2
3
4
5
6
7
8
9
10
11
12
13
#include<stdio.h>
//第一个参数指代命令个数,执行该可执行文件时传入的几个命令
//第二个参数是一个指针数组,存放了每一个命令的常量字符串
int main(int arg,char* argv[])
{
printf("arg: %d\n",arg);
for(int i =0;i<arg;i++)
{
printf("argv[%d]: %s\n",i,argv[i]);
}

return 0;
}

image-20221004175303045

有了这两个参数,我们就可以利用它写一个命令行版本的计算器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#include<stdio.h>
#include<stdlib.h>

//main函数可以带参数
//第一个参数指代命令个数,执行该可执行文件时传入的几个命令
//第二个参数是一个指针数组,存放了每一个命令的常量字符串
int main(int arg,char* argv[],char *envs[])
{
if(arg != 4)
{
printf("Usage: %s [-a|-s|-m|-d] first second\n", argv[0]);
return 0;
}
int x = atoi(argv[2]);
int y = atoi(argv[3]);
if(strcmp("-a", argv[1]) ==0)
{
printf("%d+%d=%d\n",x, y, x + y);
}
else if(strcmp("-s", argv[1]) ==0)
{
printf("%d-%d=%d\n",x, y, x - y);
}
else if(strcmp("-m", argv[1]) ==0)
{
printf("%d*%d=%d\n",x, y, x * y);
}
else if(strcmp("-d", argv[1]) ==0 && y != 0)
{
printf("%d/%d=%d\n",x, y, x / y);
}
else
{
printf("Usage: %s [-a|-s|-m|-d] first second\n", argv[0]);
}

return 0;
}

实现非常简单,其使用方法如下👇

1
2
3
4
5
6
7
8
9
10
11
[muxue@bt-7274:~/git/raspi/code/22-10-04_环境变量]$ gcc test.c -o test -std=c99
[muxue@bt-7274:~/git/raspi/code/22-10-04_环境变量]$ ./test -a 10 20
10+20=30
[muxue@bt-7274:~/git/raspi/code/22-10-04_环境变量]$ ./test -s 10 20
10-20=-10
[muxue@bt-7274:~/git/raspi/code/22-10-04_环境变量]$ ./test -m 10 30
10*30=300
[muxue@bt-7274:~/git/raspi/code/22-10-04_环境变量]$ ./test -d 30 10
30/10=3
[muxue@bt-7274:~/git/raspi/code/22-10-04_环境变量]$ ./test 1 1 1 1
Usage: ./test [-a|-s|-m|-d] first second

上面这个小程序演示了main函数的参数的作用。

看到这里,想必你应该不难理解linux系统的命令是如何使用参数的,诸如ls -l等等选项,其实都是通过main函数的参数实现的!

通过main函数的参数,可以让同一个可执行文件依据命令输出不同的结果!

3.2 使用第三个参数获取环境变量

除了上面提到的main函数前两个参数,实际上main函数还可以带第三个参数!

1
2
3
4
5
6
7
8
9
10
11
//第一个参数指代命令个数,执行该可执行文件时传入的几个命令
//第二个参数是一个指针数组,存放了每一个命令的常量字符串
//第三个参数用于导入环境变量!
int main(int arg,char* argv[],char *envs[])
{
for(int i =0;envs[i];i++)
{
printf("envs[%d]: %s\n",i,envs[i]);
}
return 0;
}

因为envs是一个指针数组,所以可以通过判空来终止for循环

image-20221004180242997

在这个数组的最后,可以看到我们刚刚执行的命令也被写入了环境变量

image-20221004180309395

除了上面这个办法,我们还可以用下面两种方式来获取环境变量

environ外部导入环境变量

C语言提供了一个environ来导入环境变量,其作用和main函数第三个参数是一样的

image-20221004180624584

1
2
3
4
5
6
extern char ** environ;
printf("get env from [environ]\n");
for(int i = 0; environ[i]; i++)
{
printf("%d: %s\n", i, environ[i]);
}

image-20221004180800767

其输出的结果也是一样的

getenv函数

1
man getenv

image-20221004180501812

这个函数就能实现一些骚操作,比如写一个只有我自己可以运行的可执行文件

1
2
3
4
5
6
7
8
9
10
11
12
13
int main(int arg,char* argv[],char *envs[])
{
char* user = getenv("USER");
if(strcasecmp(user,"muxue")!=0)//strcasecmp忽略大小写
{
printf("权限禁止!\n");
return -1;
}
printf("成功执行!\n");


return 0;
}

通过getenv函数获取到环境变量中的USER,判断其与我自己设定的user是否相同。如果不同就拒绝执行,相同才成功执行

1
2
3
4
5
[muxue@bt-7274:~/git/raspi/code/22-10-04_环境变量]$ ./test
成功执行!

[root@bt-7274:/home/muxue/git/raspi/code/22-10-04_环境变量]# ./test
权限禁止!

可以看到,哪怕是root用户也搞不来这个可执行程序!


4.关于本地变量的说明

2.2中提到了本地变量和环境变量的区别

  • 本地变量(局部)
  • 环境变量(全局)

所谓的本地变量,其实是bash内部定义的变量。

我们首先需要了解的是,linux下大部分的进程或命令都是以子进程方式运行的,其父进程都是当前打开的bash

由此可知,bash内部的本地变量,并不会被这些子进程所继承

而环境变量具有全局属性,可以被子进程继承并获取!


那么问题来了,export/echo也是命令。如果它们也是子进程,那它们是怎么获取到bash内部的本地变量,并将其导入到环境变量中的呢?

nope!实际上,这两个命令都是由bash自己执行的(调用自己的对应的函数完成功能)我们把这种命令称作内建命令

结语

关于环境变量的基本认识到这里就OVER啦。本博客是我的课堂笔记,难免会有问题,还请各位大佬指出~

QQ图片20220419103136