Linux下进程链接结构,命令行参数,环境变量
bash 是一种 shell。在 Linux 系统中,当我们在终端输入命令时,通常是在一个 shell 环境下进行的。如果这个 shell 是 bash,那么所有命令行执行的命令都是 bash 的子进程。
1.Linux下进程链接结构
进程链接补充知识:
- 所有进程都要以链表方式链接
- 进程可以在调度队列里,也可以在阻塞队列里等等
Linux中链式结构:双链表结构
C语言中双链表:
如果按照C语言设定的链表结构,然后把进程属性与链接字段放在一起,这样就会显得不优雅,过于臃肿,更重要的是可扩展性小。
所以Linux设计模式,只有链接字段,没有属性字段。
结构体中,存放两个指针,前驱指针,与后驱指针,用来表示双链表,然后进程PCB中管理一个该结构体成员,可以通过该结构体成员,使用了node结构体的双向链表进而来链接其他进程,管理起来。
Linux采用了内部数据结构,属性在数据结构外部的方法,从而对进程进行管理链接。
那么意义是什么呢?
如图:
说明了,一个进程,既可以在全局链表中,又可以在任何一个其他数据结构中,只需要加相关数据结构节点字段即可!!!
一个进程如果在运行队列就链接到queuenode中,在等待队列中就链接到waitnode中!!!进程什么状态要做什么就链接到相关字段即可!!!
原理是什么呢?如果要知道进程属性怎么办呢?知道next,prev只能找到下一个进程的link地址,该怎么找到其进程相关属性呢?
在C语言中学过:
如上图,在一个结构体中,如果只知道变量c的地址该如何找到该数据结构的起始地址呢?
我们只要知道c相对于起始地址的偏移量,就可以知道数据结构起始地址,通过c的地址减去偏移量,然后对其结果进行强转一下就可以知道,起始地址。
那么关键是怎么求偏移量呢?我们把其实地址当作0来看,对0进行强转,然后指向c变量,然后对其取地址就可以知道偏移量是多少,然后再计算即可,
在C语言中有一个宏offset就是用来求偏移量的,和上面原理一样,知道偏移量就可以求出数据结构起始地址。
所以,当我们知道进程中link地址时,只要能得出偏移量就可以算出struct task_struct的初始地址,然后就可以直接访问进程数据了。
2.命令行参数
在main函数中,存在两个参数,argc 与 argv[]。argc指的是参数个数,argv指的是参数清单。
让我们对上述代码打印看看
#include <stdio.h>
#include <string.h>
int main(int argc, char *argv[])
{
printf("argc: %d\n", argc);
for (int i = 0; i < argc; i++)
{
printf("argv[%d]: %s\n", i, argv[i]);
}
return 0;
}
结果如下:
在命令行上只输入可执行程序,结果是参数个数为1,参数清单有个刚才输入的字符串。在多输入几次观看发现,argc的参数个数就是在命令行上输入以空格为分隔符的字符串个数,acgv[]中就是这些字符串。
#include <stdio.h>
#include <string.h>
int main(int argc, char *argv[])
{
printf("argc: %d\n", argc);
for (int i = 0; argv[i]; i++)
{
printf("argv[%d]: %s\n", i, argv[i]);
}
return 0;
}
更改上述代码用argv[i]来进行控制,依然运行结果一样,说明argv[]最后一个为NULL,所以循环才会结束。
为什么要有这两个参数呢?
同一个程序,可以根据选项的不同,来表现出不同的功能!!!比如:指令中选项的实现!
如下代码:
#include <stdio.h>
#include <string.h>
int main(int argc, char *argv[])
{
if (argc != 2)
{
printf("Usage: code opt\n");
return 1;
}
if (strcmp(argv[1], "-opt1") == 0)
printf("功能1\n");
else if (strcmp(argv[1], "-opt2") == 0)
printf("功能2\n");
else if (strcmp(argv[1], "-opt3") == 0)
printf("功能3\n");
else
printf("默认功能\n");
return 0;
}
指令的本质就是可执行程序,那么指令后面的选项是怎么被可执行程序拿到的呢?
主要原因是:这个可执行程序,他所对应的命令行参数,最终会被解析出来,然后传递给这个程序,所以程序就拿到了对应的选项,目标程序内部再对选项进行判断,根据选项的不同,让程序表现出不同的风格。
以前学习过程中写的程序很单一,不需要选项,而Linux中指令程序要通过选项进行操作。
main函数的参数是谁给传递的呢?
在命令行解释器上进行输入指令加选项,他是字符串,首先会被shell拿到,根据空格为分隔符进行打散,形成一张表argv[]和元素个数argc。
在命令行上启动的程序,父进程都是谁? shell!!!启动的进程与shell都是父子关系,因为子进程会继承父进程,对于只读数据,不做修改,那么子进程没必要拷贝,直接可以跟父进程共享,所以,命令行参数被打散形成的表,后面就不会被修改,所以子进程也可以看到。
也就是说,最终在命令行上所作的解析,形式的argv这一张表,和argc这样的东西,因为一旦解析打散,就不会修改,所以对于子进程来将,子进程也可以看见这张表和argc,即可执行程序也可以看见。
所以程序在运行时,未来子进程会把从shell继承下来的argv与argc,以参数的形式传递给我们进程,所以我们的程序里,就读到shell命令行解析后对应的选项!!
也就是说,命令行参数,他的打散和形成的过程是由shell,我们的父进程来执行的,然后我们自己的程序是子进程,他是shell以fork的形式创建出来的,shell形成全局的argc , argv,打散后也不修改了,所以子进程也可以读到,然后子进程把他们传递给main函数就可以了。
那么谁调用main函数呢?
那么main函数是自己程序代码的第一个函数,入口函数,那么main也是一个函数,函数被定义出来就是要被调用的,那么谁调用main函数呢?
main函数是我们自己写的程序的入口,但不一定是系统调度我这个程序时直接从main函数开始,
CRTstartup指的是 C 运行时库的启动代码。当一个 C 程序开始执行时,首先执行的不是main函数,而是CRTstartup代码。
就相当于如下:
所以编译器,操作系统,加载器不是互相割裂的,彼此之间是有关系的。
3.环境变量
main函数里可以不带参数,也可以只带两个参数,或者可以带三个参数,第三个参数就是
char *env[]----->环境变量。
shell不光形成命令行参数表,还要形成一个环境变量的表env。
1.看一看环境变量
int main(int argc, char *argv[], char *env[])
{
for(int i = 0;env[i];i++)
{
printf("env[%d]: %s\n",i,env[i]);
}
return 0;
}
环境变量的格式就是 key=value。以key,value方式构建的,具有“全局”属性的变量叫做环境变量。
2.见一见环境变量:PATH
见一见环境变量----感受一个环境变量的特点。
指令env:查看环境变量。
PATH环境变量:指定命令的搜索路径。
echo $PATH查看PATH环境变量,
为什么我们执行自己的程序要带 ./ 。不带就会报错,找不到。
执行系统命令时,为什么不需要呢?比如:ls pwd。
实际上系统查指令会去 /usr/bin/ 下查找,我们自己的程序不在该路径下,所以找不到,必须带 ./ 。
为什么系统知道命令在/usr/bin/下?能修改让他认识我的路径吗?
shell登录时,他就有对应的环境变量PATH,告诉shell,应该去哪个路径下去查指令,
查对应环境变量指令:echo $()
PATH环境变量里面的内容,就是当shell运行任何一个命令行命令,他首先要查,shell会自动去以冒号为分隔符的多个子路径下去查,所以路径都查了,如果没有就会报错,找到了就会把该程序加载并执行。
PATH---->路径集合---->系统可执行文件的搜索集合。
自己所写的可执行程序不在这些路径下,所以报错。
如果不想带路径,让我们程序直接运行起来:
- 把可执行程序拷贝到/usr/bin/下(cp /usr/bin/ done)
- 把该可执行程序的路径添加到PATH中去
通过在命令行上直接输入: PATH = 可执行程序路径。
这样就可以让自己程序直接运行起来,不需要加 ./ 。但是这样其他指令就不能运行,比如:ls
此时,我们查看PATH变量内容:
直接就把直接路径全部给覆盖了,所以,系统命令都不能使用运行。这里只要关闭重新登陆就可以恢复。
因为环境变量是内存级的,是被加载到bash进程内,子进程继承环境变量,子进程修改不会改变父进程内容,关闭重新登陆shell,子进程就可以重新继承bash里面环境变量。
PATH环境变量从哪里来
环境变量,开始都在系统配置文件中。
我们登陆shell时,启动一个shell进程,shell读取用户和系统相关的环境变量的配置文件,形成shell自己的环境变量表,然后被子进程继承拿到环境变量PATH。
要想永久的更改PATH,先要找到配置文件,在家目录就会存在两个配置文件 .bash_profire和 .bashrc 。
只要把自己可执行程序路径放入配置文件中,在配置文件中进行修改路径,这样就可以直接像指令一样使用自己的程序!!!
指令source:在shell环境中,用于在当前环境中加载和执行脚本或配置文件。
补充知识:
进程内部都会有记录是谁启动的进程的UID,那么,你在启动进程的时候,系统怎么知道你是谁?并且把你的UID写到PCB中?
环境变量中有,所以在登陆的时候,把你自己是谁写到环境变量里,bash早就知道我这个bash是给哪一个用户提供服务的,然后启动创建进程,用户名就知道了。
3.见一见多的环境更变量
1.HOME----->当前用户对应的家目录
那么是默认就在家目录里,再设置的环境变量,还是默认先读环境变量,再把当前用户设置再家目录下?
当我们在登陆的时候,系统首先给我们用户创建对应的bash来给我们做准备,bash读取环境变量相关的配置文件,读取配置文件的本质就是要配置他自己的环境变量,bash也是一个进程,也要有cwd
bash的cwd设置为我们读到的HOME环境变量。
怎么设置?bash调用类似chdir的函数,将bash对应的工作目录更改成cwd对应的路径。
所有命令行执行命令,都是bash的子进程,这些子进程的task_struct属性从哪里来呢?
有很多属性都是拷贝自父进程的,代码共享,数据各自私有一份。其中也包括cwd,
就这是为什么在命令行上,当我自己处在某一个路径,把路径切换了,当我去执行命令时,我的路径总会是在我对应的指定的bash的路径,因为我的cwd继承了bash的cwd。
为什么默认处在家目录?
因为最开始bash读取环境变量,把环境变量设置好,bash把自己cwd设置搭配HOME环境变量中。
2.SHELL--->本质记录这个用户登陆时启动了哪一个shell
3.PWD---->保存当前进程所在的工作路径
函数getenv:获取环境变量,获取成功返回该环境变量起始位置地址,反之返回NULL。
执行下面代码:
#include<stdio.h>
#include<stdlib.h>
int main()
{
printf("%s\n",getenv("PWD"));
return 0;
}
指令pwd就是通过环境变量PWD来获取的!!!
为什么要保存呢?
进程能获得自己所在的路径,新建一个文件“log.txt”,怎么新建呢?----> getenv("PWD")/filename
方便定位当前工作路径,对文件进行操作新建。
4.USER--->表示当前使用系统的用户是谁
#include<stdio.h>
#include<stdlib.h>
int main()
{
printf("USER: %s\n",getenv("USER"));
return 0;
}
可以通过获取USER,意味着可以让我的子程序,识别用户身份,如果不是目标用户就不能使用该程序,是该目标用户才能使用该程序。
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
int main()
{
//可以让我的程序只能我运行,其他人用不了
const char *who = getenv("USER");
if(strcmp(who, "ubuntu") == 0)
{
printf("执行程序的正常命令\n");
return 0;
}
else
{
printf("无权访问\n");
return 1;
}
return 0;
}
5.OLDPWD--->切换路径后,保存上一次所在的路径
指令cd - :就可以根据OLDPWD回到上一个路径
4.理解环境变量
环境变量功能上:是什么作用就是什么作用。
环境变量是系统提供的具有“全局”属性的变量。
本地变量VS环境变量
直接在命令行上输入变量会被放入到本地变量中去,在bash进程中也会存在一张表,用于存放本地变量。
怎么同时查看本地变量与环境变量?
指令set:查看所有变量,里面有一部分是bash本身的变量
怎么把本地变量更改为环境变量呢?
指令export 名:把本地变量更改为环境变量
指令unset 名:删除一个环境变量
把本地变量变成环境变量就是把本地变量中表的元素放入环境变量中。 也可以直接导入环境变量
ecport a=100;
下面代码生成的可执行程序为bash的子进程:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
int main()
{
//先设置本地变量ISRUNNING,然后export设置为环境变量
char *isrunning = getenv("ISRUNNING");
if (isrunning == NULL)
{
while (1)
{
printf("当前进程首次启动!\n");
sleep(1);
}
}
else
{
printf("当前进程已经运行了,不需要再启动了!\n");
}
return 0;
}
如果设置为本地变量,子进程获取不到,bash不会传递给子进程,
导成环境变量如下:
说明环境变量可以被子进程继承,子进程中没有本地变量,本地变量转换为环境变量是在bash中完成的。
环境变量可以被子进程继承下去,环境变量可以被所有bash之后的进程全部看见,所以环境变量具有“全局”属性。
为什么要有环境变量?
- 系统的配置,尤其是具有指导性的配置信息,环境变量他是系统配置起效的一种表现
- 进程具有独立性!环境变量可以用来进程间传递数据(只读数据)
还有一种查看环境变量方法:environ
为什么是双指针,因为他指向环境变量表的起始位置,因为环境变量表里面也是地址,所有是双指针。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
int main()
{
for(int i = 0; __environ[i];i++)
{
printf("%s\n",__environ[i]);
}
return 0;
}