1. 命令行参数
main函数也可以带参数的,如下
#include <stdio.h>
int main(int argc, char* argv[])
{
int i = 0;
for (i = 0; i < argc; ++i)
{
printf("%d:%s\n", i, argv[i]);
}
return 0;
}
命令行整个一行是一个大的字符串,以空格作为分隔符,被分割成了5个子串。
- 第一个参数
argc
是,命令行以空格作为分隔符有几个字符串,比如上面是5个字符串,argc就是5。- 第二个参数argv是一个指针数组,保存着每个子串的地址。并且有效元素要比实际上命令行的子串多一个,最后一个一般以
NULL
结尾。
识别这些字符串子串和传参是操作系统自动帮我做的。为什么main函数要这么设计呢?
比如我们想实现一个命令行版的计算器:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(int argc, char* argv[])
{
if (argc != 4)
{
printf("Use it incorrectly, please conform to the following usage.\nUsage:%s op[-add|sub|mul|div] d1 d2", argv[0]);
}
int x = atoi(argv[2]);
int y = atoi(argv[3]);
// 一定有四个命令行参数
if (strcmp(argv[1], "-add") == 0)
{
printf("%d+%d=%d\n",x ,y ,x+y);
}
else if (strcmp(argv[1], "-sub") == 0)
{
printf("%d-%d=%d\n",x ,y ,x-y);
}
else if (strcmp(argv[1], "-mul") == 0)
{
printf("%d*%d=%d\n",x ,y ,x*y);
}
else if (strcmp(argv[1], "-div") == 0)
{
if (0 == y)
{
printf("%d/%d=error!\nZero cannot be used as the divisor.\n",x ,y);
}
else
{
printf("%d/%d=%d\n",x ,y ,x/y);
}
}
else
{
printf("Use it incorrectly, please conform to the following usage.\nUsage:%s op[-add|sub|mul|div] d1 d2", argv[0]);
}
return 0;
}
使用一下:
由此我们可以理解原来使用的命令和main函数这么设计的原因就是:
比如我们原来用的
ls
命令(这些命令就是用C语言写的!),带着命令行选项(和我们上述写的命令行计算器带选项相似), 就可以实现同一选项实现不同功能。
命令行参数(选项),可以支持各种指令级别的命令行选项设置!
2. 环境变量
2.1 环境变量的概念
基本概念:
- 环境变量(environment variables)一般是指在操作系统中用来指定操作系统运行环境的一些参数。
- 如:我们在编写C/C++代码的时候,在链接的时候,从来不知道我们的所链接的动态静态库在哪里,但是照样可以链接成功,生成可执行程序,原因就是有相关环境变量帮助编译器进行查找。
- 环境变量通常具有某些特殊用途,还有在系统当中通常具有全局特性。
命令env
查看当前操作系统所有环境变量。
系统重会存在大量的环境变量,每一个环境变量都有它自己的特殊用途用来完成特定的系统功能~!
常见环境变量:
- PATH : 指定命令的搜索路径
- HOME : 指定用户的主工作目录(即用户登陆到Linux系统中时,默认的目录)
- SHELL : 当前Shell,它的值通常是/bin/bash。
- PWD:表示当前工作目录的路径。
查看环境变量方法:
echo $NAME
NAME:你的环境变量名称
2.2 环境变量的使用和一些问题
为什么执行系统命令的时候不需要带./
,但是执行我们自己的可执行程序却需要呢?
首先使用命令
echo $PATH
查看环境变量
不用带./
的原因就是,ls等这些系统级别的指令都存储在这个环境变量里,执行的时候系统会依次检索这些目录。
如果想让我们的可执行程序不带./
就可以执行,很简单,把可执行程序所在的目录添加到环境变量即可。
使用命令
PATH=$PATH:你要添加的目标目录
(该命令是直接接到原有的环境变量之后)
此时再次来查看环境变量的路径
发现我们的可执行程序所在的目录已经在环境变量里了,来用一下~
此时,就不用带./
了捏!
如何删除呢?
直接用命令
PATH=原目录
(意思就是直接复制一份老的环境变量直接覆盖即可)
这时不带./
我们的可执行程序就又跑不了了。
如果我们干个“坏事”,把环境变量直接整没,PATH=""
然后,然后就会这样了,几乎所有命令都不能使用了,怎么办捏,系统是不是就崩了!其实不用过于担心, 重新登陆一下系统就好了(果然重启解决99%的问题)。
我们对环境变量的修改,仅仅只在内存级别的修改,我们知道内存是易失性存储器,只要系统重启就会恢复原有的模样。默认更改环境变量,只限于本次登录,如果重新登录的话环境变量会自动恢复。
如果我们直接把我们自己的可执行程序直接拷贝到默认的环境列表中,也可以做到如此效果,这个操作我们称之为程序安装。
我们在登陆Linux的时候发现
为什么普通用户默认所处目录/home/XXX
而超级用户所处/root
呢?
登陆的时候:
- 输入用户名和密码
- 认证
- 形成环境变量(PATH、PWD、HOME等)
- 根据用户名初始化
HOME=/root
,HOME=/home/XXX
cd $HOME
2.3 获取环境变量
- 学习一个调用,获取环境变量:
getenv(const char *name)
那么环境变量的作用体现在哪里呢?可以实现系统级别的过滤,比如下段简单的代码:
#include <stdio.h>
#include <stdlib.h>
int main()
{
char *user = getenv("USER");
// 如果用户不是超级用户,直接返回
if (strcmp(user, "root"))
{
printf("The user is incorrect,please switch users!\n");
return 1;
}
printf("%s is my command test\n", user);
return 0;
}
当前不是超级用户,执行非法。我们切换一下用户再次执行。
成功执行!
- 在学习命令行参数的时候我们知道,main函数可以带两个参数,那么只能带两个参数吗?main函数其实还可以带第三个参数的:
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main(int argc, char* argv[], char* env[])
{
int i = 0;
for (i = 0; env[i]; ++i)// 这个和argv相似,最后都会放一个NULL
{
printf("pid:%d,env[%d]:%s\n", getpid(), i, env[i]);
}
return 0;
}
可以看到,与我们直接在命令行使用env
命令基本相似。
系统启动我们的程序的时候,可以选择给我们的进程(main)提供两张表:
- 命令行参数表
- 环境变量表
- 如果不想使用命令行参数来查看环境变量表,可以使用C语言为我们提供的一个全局标量
environ
int main()
{
extern char **environ;
int i = 0;
for ( ; environ[i]; ++i)
{
printf("%d: %s\n", i, environ[i]);
}
return 0;
}
libc中定义的全局变量environ指向环境变量表,environ没有包含在任何头文件中,所以在使用时 要用extern声明。
执行一下,同样会得到我们想要的效果。
2.4 深入理解环境变量
上面我们做过实验,用PATH=""
将路径直接覆盖为空,会导致大部分命令使用不了,但是重新登陆之后又会恢复如初。当前我们直接更改的是bash进程内部的环境变量信息!每一次重新登陆,都会给我们形成新的bash解释器并且新的bash解释器自动从某一位置读取形成自己的环境变量表信息。
命令行启动的进程都是shell/bash的子进程,子进程的命令行参数和环境变量,是父进程bash给我们传递的!那么父进程的环境变量信息又从哪里来呢?
环境变量信息是以脚本配置文件的形式存在的。
进入家目录下,查看隐藏文件,会发现有一个.bash_profile
的文件
当我们登录时,bash会自动加载该文件,配置文件中的内容,为我们bash进程形成一张环境变量信息表!
我们可以通过一些特殊的指令,添加我们自己的环境变量:
把这种定义的变量称之为shell定义的本地变量,但是它并没有存在bash的环境变量表中。使用命令export 你的环境变量名
,把你的环境变量导出到它自己的环境表进程中,再次用env
查看发现,bash和我们的可执行程序(也就是子进程)都有了我们定义的环境变量。
也可以直接在定义环境变量的同时直接导出
export 你的环境变量名=内容
但是该自定义环境变量依旧是存储在内存里的,一旦重新登陆,又消失不见了,那么应该怎么才能永久保存呢?
再次进入
bash_profile
文件,加入我们对应的自定义环境变量:保存退出,再次重新登陆,环境变量便永久存在了
命令行是支持定义本地变量的,比如:
本地变量 vs 环境变量
- 本地变量只在bash进程内部有效,并且不会被子进程继承。
- 环境变量通过让所有子进程继承的方式,实现自身的全局性。
当我们清空环境变量时会导致大部分命令使用不了,在Linux中这大部分是用不了的命令,是在磁盘中真正存在并且需要由fork创建子进程来执行的命令。但是在shell中还有一种命令,并不会创建子进程,它的执行风险非常低,由bash自己来执行,就等同与bash内一个函数,诸如echo
、export
这样的命令依旧能够继续使用。
Linux的命令分类:
- 常规命令。需要shell、fork创建子进程,让子进程执行的命令。
- 内建命令。shell命令行的一个函数,echo就是内建命令,所以可以直接读取shell内部定义的本地变量。
2.5 环境变量相关的命令
echo
: 显示某个环境变量值export
: 设置一个新的环境变量env
: 显示所有环境变量unset
: 清除环境变量set
: 显示本地定义的shell变量和环境变量
3. 进程地址空间
3.1 基本概念
在学习C/C++时,我们学习过这样的空间布局:
用一段代码验证下:
#include <stdio.h>
#include <stdlib.h>
int un_gval;
int init_gval = 100;
int main(int argc, char* argv[], char* env[])
{
printf("code address:%p\n", main);
const char *str = "hello Linux";
printf("read only char address:%p\n", str);
printf("init global value address:%p\n", &init_gval);
char *heap1 = (char*)malloc(100);
char *heap2 = (char*)malloc(100);
char *heap3 = (char*)malloc(100);
char *heap4 = (char*)malloc(100);
printf("heap1 address:%p\n", heap1);
printf("heap2 address:%p\n", heap2);
printf("heap3 address:%p\n", heap3);
printf("heap4 address:%p\n", heap4);
printf("stack address:%p\n", &str);
printf("stack address:%p\n", &heap1);
printf("stack address:%p\n", &heap2);
printf("stack address:%p\n", &heap3);
printf("stack address:%p\n", &heap4);
int i = 0;
for (; argv[i]; ++i)
{
printf("argv[%d]:%p\n", i, argv[i]);
}
for (i = 0; env[i]; ++i)
{
printf("env[%d]:%p\n", i, env[i]);
}
return 0;
}
运行结果:
堆向上生长,栈向下生长,堆栈相对而生也得已验证。栈区虽然整体是向下生长但是局部是向上使用的。
当我们定义一个int整型取地址时,我们发现所得到地址只有一个字节,但是int却是四个字节,难道不应该得到四个字节的地址吗。
其实我们取地址一般都取到的是这个变量的地址,所以能得出一个结论:C/C++进程访问的本质是起始地址+偏移量的访问形式!
再来段代码感受一下:
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
int g_val = 100;
int main()
{
pid_t id = fork();
if (id == 0)
{
int count = 5;
while (1)
{
printf("child[%d]: %d : %p\n", getpid(), g_val, &g_val);
sleep(1);
if (count == 0)
{
g_val = 200;
printf("sub-process is changed: 100->200\n");
}
count--;
}
}
else
{
// father
while (1)
{
printf("parent[%d]: %d : %p\n", getpid(), g_val, &g_val);
sleep(1);
}
}
}
输出结果:
g_val的值未改动之前:
我们发现,输出出来的变量值和地址是一模一样的,因为子进程按照父进程为模版,父子并没有对变量进行进行任何修改。
g_val的值改动之后,父子进程,输出地址是一致的,但是变量内容不一样!能得出如下结论:
- 变量内容不一样,所以父子进程输出的变量绝对不是同一个变量!
- 但地址值是一样的,说明,该地址绝对不是物理地址!
- 在Linux地址下,这种地址叫做虚拟地址!
- 我们在用C/C++语言所看到的地址,全部都是虚拟地址!物理地址,用户一概看不到,由OS统一管理!
OS必须负责将虚拟地址转化成物理地址。
上面的图就足矣说名问题,同一个变量,地址相同,其实是虚拟地址相同,内容不同其实是被映射到了不同的物理地址!
这里的写时拷贝是在物理内存中的,由操作系统来做,并且不影响上层语言。
地址空间也要被OS管理起来,每一个进程都要有地址空间,在系统中,一定要对地址空间做管理!
如何管理地址空间呢?(经典六字)
先描述,再组织。
地址空间最终一定是一个内核的数据结构对象!就是一个内核的结构体!
在Linux中,进程/虚拟地址空间这个东西就是一个结构体,大概描述一下:
struct mm_struct
{
long code_start;
long code_end;
long data_start;
long data_end;
long heap_start;
long stack_start;
long stack_end;
}
3.2 为什么要有地址空间
让进程以统一的视角看待内存
所以任意一个进程,可以通过地址空间+页表的方式将乱序的数据内存,变成有序,并且分门别类的规划好!无论什么改动,我们只需改变映射关系即可,不用在物理内存中挨个去找,大大提高了管理内存的效率。
其实页表除了保存虚拟地址和物理地址外,还有一个访问权限字段
所以说,存在虚拟地址空间可以有效的进行进程访问内存的安全检查!
- 将进程管理和内存管理进行解耦
通过页表,让进程映射到不同的物理内存处,从而实现进程的独立性
挂起在Linux中如何体现呢?
在页表中除了访问权限字段之外,还有检测对应的物理地址是否在内存当中,如果查询页表时标记字段标记为0(假设0表示该地址不在内存中),那么就认为该进程为挂起状态。