命令行参数
int main (int argc, char* argv[])
- 命令行参数列表
argc
:参数的个数argv
:参数的清单
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;
}
命令行参数是在命令行中输入命令时,跟在命令后面的附加信息。这些参数用于向程序传递特定的指令、选项或数据,以改变程序的默认行为或提供必要的输入。
同一个程序,可以根据命令行参数、根据选项的不同,表现出不同的功能。比如:指令中选项的实现。
环境变量
基本概念
- 环境变量(environment variables)一般是指在操作系统中用来指定操作系统运行环境的一些参数。
- 如:我们在编写C/C++代码的时候,在链接的时操作候,从来不知道我们所链接的动态静态库在哪里,但是照样可以链接成功,生成可执行程序,原因就是有相关环境变量帮助编译器进行查找。
- 环境变量通常具有某些特殊用途,还有在系统当中通常具有全局特性。
- 在Linux操作系统中,环境变量是存储系统和用户相关信息的动态命名值。它们本质上是一些字符串,可以被系统进程、shell脚本以及各种应用程序访问。这些变量在系统启动时或者用户登录时被初始化,并且在整个系统运行过程中起到关键的配置作用。
重要的环境变量及其作用
1. PATH
PATH是一个非常重要的环境变量,它定义了系统在执行命令时查找可执行文件的路径列表。在终端输入一个命令时,系统会按照PATH变量中指定的目录顺序依次查找对应的可执行文件。
- 为什么系统知道,命令在/usr/bin路径下?
- 因为PATH环境变量,告诉了shell应该在哪一个路径下查找命令。
- 若想运行程序前不带路径,把其路径添加到PATH中。
- 进程启动会记录是谁启动的这个进程。
- 在启动进程的时候,系统怎么知道操作者是谁?并如何知道其id写入进程pcb中?
- 环境变量(开始是在系统的配置文件中的)
- 登陆→启动shell进程→读取用户和系统用户相关的环境变量的配置文件→形成自己的环境变量表→子进程。
2. HOME
HOME变量指定用户的主目录,用于存储用户个人文件、配置文件等。
3. LANG和LC_ALL
用于设置系统的语言环境和本地化相关信息。
-
LANG定义了系统默认的语言、字符编码等本地化设置。
-
LC_ALL是一个优先级更高的环境变量,当设置了LC_ALL时,它会覆盖LANG和其他相关的本地化环境变量。这些变量可以决定系统如何显示日期、时间、文本编码等信息。
-
示例:
若设置LANG=en_US.UTF - 8,表示系统将以美国英语和UTF - 8编码来处理文本相关内容。
4. SHELL
SHELL环境变量指定了用户所使用的shell程序。
常见的shell有bash、zsh、csh等,用于确定当用户打开一个新的终端时,系统将启动哪种shell来解释用户输入的命令。
其值通常是**/bin/bash**。
和环境变量相关的命令
- echo: 显示某个环境变量值
- export: 设置一个新的环境变量
- env: 显示所有环境变量
- unset: 清除环境变量
- set: 显示本地定义的shell变量和环境变量
环境变量的查看方法
echo $NAME
printenv
:查看所有的环境变量
printenv NAME
:查看指定环境变量env | grep NAME
环境变量的修改方法(临时修改)
-
使用export命令:
对于临时修改环境变量,可以用export命令。 -
例如:要临时将PATH变量添加一个新的目录/new/dir,可以输入
export PATH=$PATH:/new/dir
。
这种修改只在当前终端会话有效,当关闭终端后,修改就会消失。
环境变量的组织方式
每个程序都会收到一张环境表,环境表是一个字符指针数组,每个指针指向一个以’\0’ 结尾的环境字符串
通过代码如何获取环境变量
- 命令行第三个参数
#include <stdio.h>
int main(int argc, char *argv[], char *env[])
{
int i = 0;
for(; env[i]; i++){
printf("%s\n", env[i]);
}
return 0;
}
- 通过第三方变量environ获取
#include <stdio.h>
int main(int argc, char *argv[])
{
extern char **environ;
int i = 0;
for(; environ[i]; i++){
printf("%s\n", environ[i]);
}
return 0;
}
libc中定义的全局变量environ指向环境变量表,environ没有包含在任何头文件中,所以在使用时要用extern声明。
通过系统调用获取或设置环境变量
putenv
,后面讲解getenv
,本次讲解
#include <stdio.h>
#include <stdlib.h>
int main()
{
printf("%s\n", getenv("PATH"));
return 0;
}
常用getenv和putenv函数来访问特定的环境变量。
环境变量通常是具有全局属性的
- 环境变量通常具有全局属性,可以被子进程继承下去
#include <stdio.h>
#include <stdlib.h>
int main()
{
char *env = getenv("MYENV");
if(env){
printf("%s\n", env);
}
return 0;
}
直接查看,发现没有结果,说明该环境变量根本不存在
- 导出环境变量
export MYENV="hello world"
- 再次运行程序,发现结果有了!说明:环境变量是可以被子进程继承下去的!想想为什么?
环境变量的继承与作用范围
当一个进程创建子进程时,子进程会继承父进程的环境变量(具有主从属性)。
环境变量的作用范围主要取决于其是系统级还是用户级设置:
- 系统级环境变量对所有用户和进程都有效。
- 用户级环境变量对特定用户进程有效。
环境变量可以被所有bash之后的进程全部看到。
系统的配置信息,尤其是具有“指导性”的配置信息,也是系统配置信息的一种表现。进程具有独立性,环境变量可用来进程间传递数据(尤其是只读数据)。
程序地址空间
程序地址空间回顾
虚拟地址
来段代码感受一下
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int g_val = 0;
int main()
{
pid_t id = fork();
if(id < 0){
perror("fork");
return 0;
}
else if(id == 0){ //child
printf("child[%d]: %d : %p\n", getpid(), g_val, &g_val);
}else{ //parent
printf("parent[%d]: %d : %p\n", getpid(), g_val, &g_val);
}
sleep(1);
return 0;
}
输出
//与环境相关,观察现象即可
parent[2995]: 0 : 0x80497d8
child[2996]: 0 : 0x80497d8
我们发现,输出出来的变量值和地址是一模一样的,很好理解呀,因为子进程按照父进程为模版,父子并没有对变量进行任何修改。可是将代码稍加改动:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int g_val = 0;
int main()
{
pid_t id = fork();
if(id < 0){
perror("fork");
return 0;
}
else if(id == 0){ //child,子进程肯定先跑完,也就是子进程先修改,完成之后,父进程再读取
g_val=100;
printf("child[%d]: %d : %p\n", getpid(), g_val, &g_val);
}else{ //parent
sleep(3);
printf("parent[%d]: %d : %p\n", getpid(), g_val, &g_val);
}
sleep(1);
return 0;
}
输出结果:
//与环境相关,观察现象即可
child[3046]: 100 : 0x80497e8
parent[3045]: 0 : 0x80497e8
我们发现,父子进程,输出地址是一致的,但是变量内容不一样! 能得出如下结论:
- 变量内容不一样,所以父子进程输出的变量绝对不是同一个变量,但地址值是一样的,说明,该地址绝对不是物理地址!
- 在Linux地址下,这种地址叫做 虚拟地址
- 我们在用C/C++语言所看到的地址,全部都是虚拟地址! 物理地址用户一概看不到,由OS统一管理
OS必须负责将 虚拟地址 转化成 物理地址 。
5-4 进程地址空间
所以之前说 ‘程序的地址空间’ 是不准确的,准确的应该说成 进程地址空间 ,那该如何理解呢?看图:
说明:
-
上面的图就足矣说明问题,同一个变量,地址相同,其实是虚拟地址相同,内容不同其实是被映射到了不同的物理地址!
-
如何理解虚拟地址空间?
让每一个进程都认为自己独占系统物理内存,进程彼此之间不知道、不关心对方的存在,从而实现一定程度的隔离。所谓进程虚拟地址空间,本质上是一个内核数据结构对象。 -
如何理解区域划分?
只要告诉开始和结束即可。地址本质就是一个数字,可被保存在unsigned long空间范围内的地址可以随便用,不必详细记录范围内地址。 -
虚拟内存管理方案:struct mm_struct + 页表
虚拟内存管理
描述linux下进程的地址空间的所有的信息的结构体是 mm_struct
(内存描述符)。每个进程只有一个mm_struct
结构,在每个进程的task_struct结构中,有一个指向该进程的结构。
struct task_struct
{
/*...*/
struct mm_struct *mm; //对于普通的用户进程来说该字段指向他的虚拟地址空间的用户空间部分,对于内核线程来说这部分为NULL。
struct mm_struct *active_mm; // 该字段是内核线程使用的。当该进程是内核线程时,它的mm字段为NULL,表示没有内存地址空间,可也并不是真正的没有,这是因为所有进程内核的映射都是一样的,内核线程可以使用任意进程的地址空间。
/*...*/
}
可以说,mm_struct结构是对整个用户空间的描述
。每一个进程都会有自己独立的mm_struct,这样每一个进程都会有自己独立的地址空间才能互不干扰。先来看看由task_struct
到mm_struct
,进程的地址空间的分布情况:
定位mm_struct文件所在位置和task_struct所在路径是一样的,不过他们所在文件是不一样的,mm_struct所在的文件是mm_types.h。
struct mm_struct
{
struct vm_area_struct *mmap; /* 指向虚拟区间(VMA) 链表 */
struct rb_root mm_rb; /* 红黑结构体树的虚拟地址空间 */
unsigned long task_size; /*具有该结构体的进程的虚拟地址空间的大小*/
/* 代码段、数据段、堆栈段、参数段及环境段的起始和结束地址。*/
unsigned long start_code, end_code, start_data, end_data;
unsigned long start_brk, brk, start_stack;
unsigned long arg_start, arg_end, env_start, env_end;
/*...*/
}
那既然每一个进程都会有自己独立的mm_struct,操作系统肯定是要将这么多进程的mm_struct组织起来的。虚拟空间的组织方式有两种:
- 当虚拟区间少的时候采取链表,由mmap指针指向这个链表;
- 当虚拟区间多的时候采取红黑树进行管理,由mm_rb指向这棵树。
linux内核使用vm_area_struct
结构来表示一个独立的虚拟内存区域(VMA),由于每个不同质的虚拟内存区域功能和内部机制都不同,因此一个进程使用多个vm_area_struct结构来分别表示不同类型的虚拟内存区域。
上面提到的两种组织方式使用的就是vm_area_struct结构来连接各个VMA,方便进程快速访问。
struct vm_area_struct {
struct mm_struct *vm_mm; /* 所属的 mm_struct */
unsigned long vm_start; //虚存区起始
unsigned long vm_end; //虚存区结束
struct vm_area_struct *vm_next, *vm_prev; //前后指针
struct rb_node vm_rb;
pgprot_t vm_page_prot; //标志位
unsigned long vm_flags;
struct list_head anon_vma_chain;
struct anon_vma *anon_vma;
const struct vm_operations_struct *vm_ops; //vma对应的实际操作
unsigned long vm_pgoff; //文件的映射偏移量
struct file * vm_file; //映射的文件
void * vm_private_data; //私有数据
/*...*/
}
所以我们可以对上图在进行更细致的描述,如下图所示:
页表
页表是操作系统用于实现虚拟内存管理中虚拟地址到物理地址映射的一种数据结构。
在分页系统中,每个进程都有自己独立的页表,其主要功能是记录虚拟页(由进程的虚拟地址空间划分而来)与物理页(物理内存划分之后的单元)之间的映射关系,同时还包含了每个页面的访问权限等信息。
页表项
- 物理页号:指明虚拟页对应物理页在物理内存中的位置。
- 访问权限号:用于控制对该页面的访问方式,常见的权限包括可读(R)、可写(W)、可执行(E)等。
- 存在位:用于指示该虚拟页是否已经加载到物理内存中。
- 脏位:当一个页面被修改时,脏位会被置为1,操作系统可以根据脏位来判断是否需要将修改后的页面写回磁盘。
父进程在执行过程中,当需要创建子进程时,子进程会继承父进程的虚拟地址布局。
在初始阶段,子进程的页表项和父进程的页表指向相同的物理页面,并且这些页表项的权限设置为只读。(这使得子进程可以访问和父进程相同的代码。)
当子进程或父进程尝试对共享页面进行写入操作时,会触发写时复制机制。(引发一个缺页中断)
处理缺页中断时,会为发生写入操作的进程分配一个新的物理页面,将原来共享的页面复制并更新,使其指向新建物理页面,并将该页面权限改为可读写。
进程虚拟地址空间的作用
- 提供内存抽象与隔离
- 抽象:为每个进程提供了一个简单、统一的内存视图,进程无需关心物理内存的复杂细节。
- 隔离:每个进程的虚拟地址空间相互独立,有效防止进程之间干扰。
- 支持进程的独立运行和动态内存管理
- 便于程序链接和加载
页表的作用
- 实现虚拟地址到物理地址的映射
- 提供内存保护和访问控制