文章目录
- 一、了解冯诺依曼体系结构
- 1、概念
- 2、对数据层面
- 3、实例
- 二、操作系统
- 1、概念
- 2、设计OS的目的
- 3、定位
- 4、操作系统怎么管理?
- 三、进程
- 1、概念
- 2、怎么管理进程
- 3、描述进程-PCB
- 4、描述进程怎么运行(粗略)
- 5、进程属性
- 6、创建子进程
- 7、创建多进程
- 8、进程状态
- 9、进程的优先级
- 10、进程切换
- 11、命令行参数
- 12、环境变量
- 13、虚拟地址空间
- 14、进程创建
- 15、进程终止
- 16、进程替换
一、了解冯诺依曼体系结构
1、概念
冯·诺依曼体系结构是由数学家冯·诺依曼在1945年首次提出的,它规定了计算机的基本组成部分以及各部分之间的数据流向。这套理论包括采用二进制逻辑、程序存储执行以及计算机由五个基本部分组成,即运算器、控制器、存储器、输入设备和输出设备。
- 运算器:用于完成各种算术运算、逻辑运算和数据传送等数据加工处理。它是计算机中进行数据加工的核心部件。
- 控制器:用于控制程序的执行,是计算机的大脑。它根据存放在存储器中的指令序列(程序)进行工作,并由一个程序计数器控制指令的执行。控制器具有判断能力,能根据计算结果选择不同的工作流程。运算器和控制器共同组成计算机的中央处理器(CPU)。
- 存储器:用于记忆程序和数据。在冯·诺依曼体系结构中,程序和数据以二进制代码形式不加区别地存放在存储器中,存放位置由地址确定。这种设计使得程序和数据可以共享同一块存储空间,从而提高了计算机的灵活性和效率。(这里指的是内存)
- 输入设备:用于将数据或程序输入到计算机中。常见的输入设备包括键盘、鼠标等。
- 输出设备:用于将数据或程序的处理结果展示给用户。常见的输出设备包括显示器、打印机等。
2、对数据层面
不考虑缓存情况,这里的CPU能且只能对内存进行读写,不能访问外设(输入或输出设备),外设(输入或输出设备)要输入或者输出数据,也只能写入内存或者从内存中读取,即所有的设备只能和内存直接打交道。
3、实例
使用qq发信息
二、操作系统
1、概念
任何计算机系统都包含一个基本的程序集合,称为操作系统(OS)。笼统的理解,操作系统包括:
内核(进程管理,内存管理,文件管理,驱动管理)
其他程序(例如函数库,shell程序等等)
2、设计OS的目的
- 与硬件交互,管理所有的软硬件资源。
- 为用户程序(应用程序)提供一个良好的执行环境。
3、定位
在整个计算机软硬件架构中,操作系统的定位是:一款纯正的“搞管理”的软件。
4、操作系统怎么管理?
总结:
计算机管理硬件
- 描述起来,用struct结构体
- 组织起来,用链表或其他高效的数据结构
三、进程
1、概念
课本概念:程序的一个执行实例,正在执行的程序等。
内核观点:担当分配系统资源(CPU时间,内存)的实体。
2、怎么管理进程
和操作系统管理硬件一样:先描述,再组织。
3、描述进程-PCB
(1)进程信息被放在一个叫做进程控制块的数据结构中,可以理解为进程属性的集合。
(2)task_struct是PCB的一种:在Linux中描述进程的结构体叫做task_struct,task_struct是Linux内核的一种数据结构,它会被装载到RAM(内存)里并且包含着进程的信息。
(3)task_ struct内容分类
- 标示符: 描述本进程的唯一标示符,用来区别其他进程。
- 状态: 任务状态,退出代码,退出信号等。
- 优先级: 相对于其他进程的优先级。
- 程序计数器: 程序中即将被执行的下一条指令的地址。
- 内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
- 上下文数据: 进程执行时处理器的寄存器中的数据[休学例子,要加图CPU,寄存器]。
- I/O状态信息: 包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。
- 记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。
- 其他信息
4、描述进程怎么运行(粗略)
综上:进程 = 内核的数据结构 + 代码数据。
5、进程属性
(1)进程标识符
- 进程id:PID
- 父进程id: PPID
(2)查看进程信息
进入/proc系统文件查看
具体数字为进程PID,如查看PID为1的进程,cd /proc/1。
(3)进程中的exe和cwd
exe:指向的是磁盘中的可执行文件。
运行时可执行文件存在时:
运行时可执行文件不存在时:
cwd:指的是当前工作的路径,如创建文件时就会使用当前的工作路径。
通过ps指令进行查看进程
功能:
显示当前系统中的进程状态。
用法:
ps [选项][PID等信息]
常用选项:
ps -u [username]:显示指定用户的进程信息。
ps -p [PID]:显示指定PID的进程信息。
ps -C[cmdname]:显示指定命令名的进程信息。
ps -o [format]:自定义输出格式,例如ps -o pid,user,cmd只显示PID、用户和命令。
ps aux:这是一个非常常用的组合选项,a显示与终端相关的所有进程(包括其他用户的进程),u以用户为中心的格式显示进程信息,x显示没有控制终端的进程。
使用:
启动一个进程,通过ps结合grep指令打印进程信息
ps -ajx | head -1 && ps ajx | grep test01 | grep -v grep
在linux系统启动后任何进程都是由其父进程创建出来的。
6、创建子进程
(1)创建子进程函数
pid_t fork(void)
(包含在头文件#include <unistd.h>
中)。
(2)返回值
有两个返回值,对于父进程返回子进程PID,对于子进程返回 0(为什么出现两个返回值下面进行解释)。
(3)使用
getpid():获取当前进程PID
getppid():获取当前进程PPID
头文件和fork()一样
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<sys/types.h>
4 int main()
5 {
6 pid_t pid = fork();
7 printf("返回值:%d\n",pid);
8 while(1)
9 {
10
11 if(pid == 0)
12 {
13 printf("我是一个子进程,pid:%d ppid:%d\n",getpid(),getppid());
14 }
15 else
16 {
17 printf("我是一个父进程,pid:%d ppid:%d\n",getpid(),getppid());
18 }
19 sleep(1);
20 }
21
22
23
24 return 0;
25 }
解释为什么会出现两个返回值:
进程 = 内核数据结构+代码数据
一般对于父子进程来说:子进程拷贝父进程的内核数据结构(子进程的一些属性会修改),代码共享父子之间共享,数据独有一份(因为进程运行间具有很强的独立性,互不干扰)。
7、创建多进程
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<sys/types.h>
4
5 void func()
6 {
7 while(1)
8 {
9
10 sleep(1);
11 printf("我是一个子进程,pid:%d ppid:%d\n",getpid(),getppid());
12 }
13 }
14 int main()
15 {
16 int arr[10]; //记录子进程id
17 for(int i = 0; i < 10; i++)
18 {
19 pid_t pid = fork(); //创建子进程
20 arr[i] = pid;
21 if(pid == 0) //如果是子进程就走这个逻辑
22 func();
23 }
24
25 printf("我是一个父进程,pid:%d ppid:%d\n",getpid(),getppid());
26 printf("我的子进程:");
27 for(int i = 0; i < 10; i++) //输入子进程
28 {
29 printf(" %d ",arr[i]);
30 }
31 printf("\n");
32 while(1); //不让父进程结束
33 return 0;
34 }
创建这些进程后,谁先运行呢?
这是由os的调度器决定的。
8、进程状态
(1)进程状态图
时间片:
进程的时间片(Time Slice 或 TimeQuantum)是操作系统在多任务处理(Multitasking)或多线程处理(Multithreading)环境下,分配给每个进程或线程的一段固定时间,用于其执行。这种机制允许操作系统公平地分配CPU资源,确保所有进程或线程都能获得执行机会,从而避免某个进程或线程长时间占用CPU资源,导致其他进程或线程长时间等待。
(2)理解进程状态图
(3)Linux内核源代码中对状态的划分
/*
* The task state array is a strange "bitmap" of
* reasons to sleep. Thus "running" is zero, and
* you can test for combinations of others with
* simple bit tests.
*/
static const char * const task_state_array[] = {
"R (running)", /* 0 */
"S (sleeping)", /* 1 */
"D (disk sleep)", /* 2 */
"T (stopped)", /* 4 */
"t (tracing stop)", /* 8 */
"X (dead)", /* 16 */
"Z (zombie)", /* 32 */
};
R运行状态(running): 并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列 里。
S睡眠状态(sleeping): 意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡眠 (interruptible sleep))。
D磁盘休眠状态(Disk sleep)有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个状态的 进程通常会等待IO的结束。
T停止状态(stopped): 可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行。
t 跟踪(tracing stop)状态。:当进程正在被调试器(如gdb)跟踪时,如果调试器在进程中的某个位置设置了断点,当进程运行到该断点时,它会停下来并进入跟踪状态。此时,进程的状态就会显示为‘t’。
X死亡状态(dead):这个状态只是一个返回状态,你不会在任务列表里看到这个状态。
(4)Z(zombie)-僵尸进程
补充知识:
进程退出:
1、代码不会执行了,首先退出程序代码和数据。
2、进程退出要有退出信息,通过task_strcut保存.
3、操作系统要维护task_strcut,方便用户获取进程退出信息。
僵死状态(Zombies)是一个比较特殊的状态。当进程退出并且父进程没有读取到子进程退出的返回代码时就会产生僵死(尸)进程,此时需要维护子进程的task_strcut,等待父进程读取子进程退出的返回代码,当父进程读取后子进程就会自动退出。
僵死进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码。
所以,只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程进入Z状态。
如下面程序:
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<sys/types.h>
4
5 int main()
6 {
7 pid_t pid = fork();
8 while(1)
9 {
10 if(pid == 0)
11 {
12 printf("我是一个子进程,pid:%d ppid:%d\n",getpid(),getppid());
13 }
14 else if(pid > 0)
15 {
16 printf("我是一个父进程,pid:%d ppid:%d\n",getpid(),getppid());
17 }
18 sleep(1);
19 }
20
21 return 0;
22 }
使用指令kill -9 pid 结束子进程后子进程进入僵尸状态。
僵尸进程危害
进程的退出状态必须被维持下去,因为他要告诉关心它的进程(父进程),你交给我的任务,我办的怎么样了。可父进程如果一直不读取,那子进程就一直处于Z状态?是的!
维护退出状态本身就是要用数据维护,也属于进程基本信息,所以保存在task_struct(PCB)中,换句话 说,Z状态一直不退出,PCB一直都要维护?是的!
那一个父进程创建了很多子进程,就是不回收,是不是就会造成内存资源的浪费?是的!因为数据结构对象本身就要占用内存,想想C中定义一个结构体变量(对象),是要在内存的某个位置进行开辟空
间! 内存泄漏?是的!
进程等待
进程等待的必要性:
对于上述的子进程退出父进程一直不读取子进程退出的返回码所导致的僵尸进程,父进程等待子进程退出然后读取子进程退出的返回码就是一个很好的解决办法。
如何进行进程等待?
wait()方法。
返回值: 成功返回被等待进程pid,失败返回-1。
参数: 输出型参数,获取子进程退出状态,不关心则可以设置成为NULL。
waitpid方法
返回值:
当正常返回的时候waitpid返回收集到的子进程的进程ID; 如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0; 如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;
参数:
pid: pid=-1,等待任一个子进程。与wait等效。 Pid>0.等待其进程ID与pid相等的子进程。
status:
WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
options: WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID。
status参数解析:
- 低7位:表示子进程退出时的终止信号编号。在Linux中,信号是一种软件中断,用于通知进程某个事件的发生。信号编号是一个整数,每个信号都有一个唯一的编号。
- 第8位:称为coredump标志位。当该位为1时,表示子进程因为接收到某些信号(如SIGSEGV)而产生了核心转储文件。核心转储文件是操作系统在程序异常终止时,将程序当时的内存状态记录下来并保存在一个文件中的行为。
0:表示子进程没有产生核心转储文件。
1:表示子进程产生了核心转储文件
求coredump位的值:((status >> 7) & 0x1- 次低8位:代表子进程的退出状态码,即进程退出时的退出码。通常,0表示成功完成,而非0的值表示出现了错误或异常情况。这个退出状态码可以被其他进程、shell脚本或程序捕获并进行处理。
- 求退出信息
WIFEXITED(status):检查子进程是否正常退出。如果子进程是因为接收到信号而终止的,则该函数返回0;否则返回非0值。
WEXITSTATUS(status):如果子进程正常退出,该函数返回子进程的退出状态码(即次低8位的值)。
WIFSIGNALED(status):检查子进程是否因为接收到信号而终止。如果是,则返回非0值;否则返回0。
WTERMSIG(status):如果子进程因为接收到信号而终止,该函数返回导致子进程终止的信号的编号。
wait/waitpid退出情况分析:
- 如果子进程已经退出,调用wait/waitpid时,wait/waitpid会立即返回,并且释放资源,获得子进程退出信息。
- 如果在任意时刻调用wait/waitpid,子进程存在且正常运行,则进程可能阻塞。
- 如果不存在该子进程,则立即出错返回。
阻塞式等待
直到子进程结束才继续向下执行其他代码。
1 #include<stdio.h>
2 #include<unistd.h>
3 #include <sys/wait.h>
4 #include<sys/types.h>
5
6 void func()
7 {
8 printf("我是一个子进程\n");
9 }
10
11
12 int main(int argc ,char * argv[])
13 {
14 pid_t pid = fork();
15
16 if(pid == 0)
17 {
18 func();
19 sleep(5);
20 }
21 else if(pid > 0)
22 {
23 int status = 0;
24 pid_t ret = waitpid(-1, &status, 0);//阻塞式等待,等待5S
25 if(ret > 0)
26 {
27 printf("等待成功,子进程id:%d\n",ret);
28 }
29 else if(ret == 0)
30 {
31 printf("没有子进程等待\n");
32 }
33 else
34 {
35 printf("出现错误\n");
36 }
37 }
38 return 0;
39 }
非阻塞等待
循环的等待子进程退出,在等待的过程父进程可以做一些自己的工作。
1 #include<stdio.h>
2 #include<unistd.h>
3 #include <sys/wait.h>
4 #include<sys/types.h>
5
6 void func()
7 {
8 while(1)
9 {
10 printf("我是一个子进程\n");
11 sleep(1);
12 }
13 }
14
15
16 int main(int argc ,char * argv[])
17 {
18 pid_t pid = fork();
19
20 if(pid == 0)
21 {
22 func();
23 }
24 else if(pid > 0)
25 { while(1)
26 {
27 int status = 0;
28 pid_t ret = waitpid(-1, &status, WNOHANG);//非阻塞等待
29
30 if(ret > 0)
31 {
32 printf("等待成功\n");
33 break;
34 }
35 else if(ret < 0)
36 {
37 printf("等待子进程失败\n");
38 }
39 else
40 {
41 printf("进程还没退出\n");
42 }
43 sleep(1);
44 }
45 }
46 return 0;
47 }
(5)孤儿进程
父进程如果提前退出,那么子进程后退出,进入Z之后,那该如何处理呢?父进程先退出,子进程就称之为“孤儿进程”,孤儿进程被1号init进程领养,当然要有init进程回收喽。
如下面程序:
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<sys/types.h>
4
5 int main()
6 {
7 pid_t pid = fork();
8 while(1)
9 {
10 if(pid == 0)
11 {
12 printf("我是一个子进程,pid:%d ppid:%d\n",getpid(),getppid());
13 }
14 else if(pid > 0)
15 {
16 printf("我是一个父进程,pid:%d ppid:%d\n",getpid(),getppid());
17 }
18 sleep(1);
19 }
20
21 return 0;
22 }
使用指令kill -9 pid 结束父进程后:
9、进程的优先级
(1)概念
- cpu资源分配的先后顺序,就是指进程的优先权(priority)。
- 优先权高的进程有优先执行权利。配置进程优先权对多任务环境的linux很有用,可以改善系统性能。
- 还可以把进程运行到指定的CPU上,这样一来,把不重要的进程安排到某个CPU,可以大大改善系统整体性能。
(2)查看进程的优先级
运行程序后,输入指令 ps -l
查看
关注下面两个信息
PRI :代表这个进程可被执行的优先级,其值越小越早被执行
NI :代表这个进程的nice值
PRI and NI
- PRI也还是比较好理解的,即进程的优先级,或者通俗点说就是程序被CPU执行的先后顺序,此值越小 进程的优先级别越高。
- 那NI呢?就是我们所要说的nice值了,其表示进程可被执行的优先级的修正数值,
- PRI值越小越快被执行,那么加入nice值后,将会使得PRI变为:PRI(new)=PRI(old)+nice
这样,当nice值为负值的时候,那么该程序将会优先级值将变小,即其优先级会变高,则其越快被执行。- 所以,调整进程优先级,在Linux下,就是调整进程nice值 nice其取值范围是-20至19,一共40个级别。
PRI vs NI
- 需要强调一点的是,进程的nice值不是进程的优先级,他们不是一个概念,但是进程nice值会影响到进程的优先级变化。
- 可以理解nice值是进程优先级的修正修正数据
(3)修改进程的优先级
在 Linux 下,可以使用 renice 命令来修改nice 值。nice 值是一个介于 -20(最高优先级)到 19(最低优先级)之间的整数。
renice [nice_value] -p [pid]
nice_value:你想要设置的新 nice 值。
-p:指定要修改优先级的进程 ID(PID)
如修改nice为-10
10、进程切换
(1)定义
进程切换是指在多任务操作系统中,由于CPU时间片轮转或者出现中断事件,导致当前运行的进程需要被暂停,而另一个进程需要被调度并开始执行的过程。当一个进程被切换出去时,操作系统会保存该进程的上下文信息(如寄存器值、程序计数器等),然后加载另一个进程的上下文信息,使其可以继续执行。
(2)触发原因
进程切换可以由多种原因触发,包括但不限于:
- 时间片轮转:操作系统为每个进程分配一个固定的时间片,当时间片用完时,当前进程就会被挂起,操作系统选择另一个进程继续执行。
- 中断处理:当硬件或软件中断发生时,操作系统会暂停当前进程,转而处理中断,处理完成后可能会选择另一个进程继续执行。
- 阻塞:当进程需要等待某个资源(如I/O操作)时,它可能会被阻塞,此时操作系统会选择另一个进程继续执行。
(3)上下文信息
在进程切换过程中,操作系统需要保存和恢复进程的上下文信息。上下文信息包括:
- 寄存器值:CPU中的寄存器用于存储临时数据和指令地址,进程切换时需要保存当前进程的寄存器值,并在恢复进程时重新加载。
- 程序计数器:程序计数器是一个特殊的寄存器,用于存储当前正在执行的指令的地址或下一条指令的地址。进程切换时需要保存当前进程的程序计数器值,以便在恢复进程时从正确的位置开始执行。
- 内存管理信息:包括进程的地址空间、页表等,用于确保进程在切换后能正确访问其内存空间。
(4)影响因素
进程切换的性能受到多种因素的影响,包括但不限于:
- 硬件架构:不同硬件架构的CPU对进程切换的支持程度不同,例如,一些架构提供了专门的硬件指令来加速进程切换。
- 操作系统内核:操作系统内核的设计和实现方式也会影响进程切换的性能。例如,内核中使用的数据结构、调度算法等都会影响进程切换的效率。
- 进程数量:系统中进程的数量越多,进程切换的频率就越高,对系统性能的影响就越大。
(5)调度算法
调度算法是操作系统用于选择下一个要执行的进程的策略。常见的调度算法包括:
- 先来先服务(FCFS):按照进程到达的顺序进行调度。
- 短作业优先(SJF):选择预计执行时间最短的进程进行调度。
- 优先级调度:根据进程的优先级进行调度,优先级高的进程优先执行。
- 时间片轮转(RoundRobin):为每个进程分配一个固定的时间片,时间片用完后进行轮转调度。
(6)linux下的调度算法
下图是Linux2.6内核中进程队列的数据结构
一个CPU拥有一个runqueue
如果有多个CPU就要考虑进程个数的负载均衡问题。
优先级
普通优先级:100~139(我们都是普通的优先级,想想nice值的取值范围,可与之对应!)
实时优先级:0~99(不关心)
活动队列
时间片还没有结束的所有进程都按照优先级放在该队列 nr_active: 总共有多少个运行状态的进程 queue[140]:
一个元素就是一个进程队列,相同优先级的进程按照FIFO规则进行排队调度,所以,数组下 标就是优先级!
从该结构中,选择一个最合适的进程,过程是怎么的呢?
- 从0下表开始遍历queue[140]
- 找到第一个非空队列,该队列必定为优先级最高的队列
- 拿到选中队列的第一个进程,开始运行,调度完成!
- 遍历queue[140]时间复杂度是常数!但还是太低效了! bitmap[5]:一共140个优先级,一共140个进程队列,为了提高查找非空队列的效率,就可以用5*32个
比特位表示队列是否为空,这样,便可以大大提高查找效率!
过期队列
- 过期队列和活动队列结构一模一样 。
- 过期队列上放置的进程,都是时间片耗尽的进程以及新进程。
- 当活动队列上的进程都被处理完毕之后,对过期队列的进程进行时间片重新计算。
active指针和expired指针
- active指针永远指向活动队列 expired指针永远指向过期队列
- 可是活动队列上的进程会越来越少,过期队列上的进程会越来越多,因为进程时间片到期时一直都存在的。 没关系,在合适的时候,只要能够交换active指针和expired指针的内容,就相当于有具有了一批新的活动进程!
总结
在系统当中查找一个最合适调度的进程的时间复杂度是一个常数,不随着进程增多而导致时间成本增 加,我们称之为进程调度O(1)算法。
11、命令行参数
(1)例子
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<sys/types.h>
4
5 int main(int argc ,char * argv[])
6 {
7 for(int i = 0; i < argc; i++)
8 {
9 printf("%d: ",i);
10 printf("%s\n",argv[i]);
11 }
12 return 0;
13 }
argc : 参数个数
argv: 参数的清单
(2)命令行参数传递过程
- 用户输入: 用户在命令行中输入一个命令及其参数,例如./myprogram arg1 arg2。
- shell解析:shell(如bash)会解析用户输入的命令和参数。 shell会识别出命令名(./myprogram)和参数列表(arg1 arg2)。
- 创建子进程: shell通过调用fork()系统调用来创建一个新的子进程。
子进程是父进程(shell)的副本,但有自己的地址空间和进程ID。- 设置命令行参数: 在创建子进程后,shell会设置子进程的命令行参数。
命令行参数通常存储在一个字符串数组中,称为argv(argument vector)。 argv[0]通常是程序的名称(如./myprogram),argv[1]是第一个参数(如arg1),argv[2]是第二个参数(如arg2),依此类推。> 数组的最后一个元素是一个空指针(NULL),用于标记数组的结束。- 执行程序: shell通过调用exec()系列函数之一(如execl(), execle(), execlp(), execv(), execve(), execvp()等)来执行子进程中的程序。exec()函数会用新的程序替换当前进程的内存映像,但保留进程ID。 在执行新程序时,exec()函数会将之前设置的命令行参数(argv)传递给新程序。
- 程序访问参数:在新程序中,可以通过main()函数的参数来访问命令行参数。 main()函数的原型通常是int main(int argc, char *argv[], char *envp[])。 argc(argument count)表示命令行参数的数量(包括程序名)。 argv是一个指向字符串数组的指针,每个字符串都是一个命令行参数。 envp是一个指向环境变量字符串数组的指针(在某些实现中,main()函数可能不包括envp参数,但可以通过其他方式访问环境变量)
12、环境变量
(1)概念
环境变量(environment variables)一般是指在操作系统中用来指定操作系统运行环境的一些参数
如:我们在编写C/C++代码的时候,在链接的时候,从来不知道我们的所链接的动态静态库在哪里,但 是照样可以链接成功,生成可执行程序,原因就是有相关环境变量帮助编译器进行查找。 环境变量通常具有某些特殊用途,还有在系统当中通常具有全局特性。
(2)环境变量从哪里来的
启动shell进程时->读取系统相关和环境变量的配置文件->形成自己的环境变量表。
(3)和环境变量相关的命令
- echo: 显示某个环境变量值
- export: 设置一个新的环境变量
- env: 显示所有环境变量
- unset: 清除环境变量
- set: 显示本地定义的shell变量和环境变量
(3)显示所有环境变量
(4)常见的环境变量
PATH:指定命令的搜索路径。
如:当我们执行一个指令时,shell会在PATH的路径查找指令。
在执行我们自己的程序时需要在可执行文件前指定路径,如“./test”,那么我们怎么样才能不用指定路径?
在环境变量中加入我们自己程序的路径就可以了。
HOME:
描述:当前用户的主目录路径。
USER:
描述:当前登录的用户名。
PWD:
描述:当前工作目录的完整路径。
SHELL:
描述:当前用户使用的shell类型。
(5)环境变量的组织方式
每个程序都会收到一张环境表,环境表是一个字符指针数组,每个指针指向一个以’\0’结尾的环境字符串。
(6)环境变量的全局属性
怎么体现?
环境变量能被子进程继承。
继承的过程
进程创建: 当父进程创建一个新的子进程时,系统会为子进程分配一个新的进程ID(PID)和相关的进程控制块(PCB)。子进程会复制父进程的地址空间(包括代码段、数据段、堆和栈等),以及进程控制块中的大部分信息。
环境变量存储:在父进程的地址空间中,环境变量通常存储在栈的上方,与命令行参数一起被维护。 这些环境变量在父进程的进程控制块(PCB)中也有相应的记录。
继承机制: 当子进程被创建时,它会复制父进程的虚拟地址空间,包括存储环境变量的部分。 因此,子进程能够访问并继承父进程的环境变量
为什么会有全局属性
a.系统的配置信息,尤其是具有“指导性”的配置信息,他是系统配置起效的一种表现
b.进程具有独立性!环境变量可以用来进程间传递数据(只读数据!)
(7)通过代码获取环境属性
a.命令行第三个参数
int main(int argc, char *argv[], char *env[])
b.通过第三方变量environ获取
extern char **environ;
libc中定义的全局变量environ指向环境变量表,environ没有包含在任何头文件中,所以在使用时 要用extern声明。
(8)通过系统调用获取或设置环境变量
获取环境变量:putenv
- 函数原型:char* getenv(const char* env_var); 头文件:需要包含<stdlib.h>和<stdio.h>。
- 功能:从环境中取字符串,获取环境变量的值。它搜索宿主环境(操作系统)提供的环境列表,匹配env_var所指向的C字符串,并返回指向与匹配的列表成员关联的C字符串的指针。
- 返回值:如果找到指定的环境变量,则返回指向该环境变量值的指针;如果未找到,则返回NULL。
1 #include <stdlib.h>
2 #include<stdio.h>
3
4 int main()
5 {
6 char *ret = getenv("PATH");
7 if(ret)
8 printf("%s\n",ret);
9
10 return 0;
11 }
设置环境变量:putenv
- 头文件:需要包含<stdlib.h>。
- 功能:putenv函数用于将字符串envvar(或string)添加到当前环境中。该字符串的格式通常为name=value,其中name是环境变量的名称,value是环境变量的值。如果指定的环境变量已经存在,则putenv会修改其值;如果不存在,则创建一个新的环境变量。
- 返回值:如果函数执行成功,则返回0;如果发生错误(如内存不足),则返回-1。
1 #include <stdlib.h>
2 #include<stdio.h>
3
4 int main()
5 {
6 char *ret = getenv("PATH");
7 if(ret)
8 printf("%s\n",ret);
9
10 return 0;
11 }
13、虚拟地址空间
(1)观察程序下面现象
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<sys/types.h>
4
5 int gval = 0;
6
7 int main()
8 {
9 pid_t pid = fork();
10 if(pid == 0)
11 {
12 gval += 10;
13 printf("我是一个子进程,pid:%d ppid:%d, gval = %d",getpid(),getppid(),gval);
14 printf("&gval = %p\n",&gval);
15 }
16 else if(pid > 0)
17 {
18 printf("我是一个父进程,pid:%d ppid:%d,gval = %d",getpid(),getppid(),gval);
19 printf("&gval = %p\n",&gval);
20 }
21
22 return 0;
23 }
现象:
父进程的gval和子进程的gval的值不同(因为进程的数据是独立的,父进程和子进程各有一份gval),但是它们的地址值却是一样。
结论:
- 变量内容不一样,所以父子进程输出的变量绝对不是同一个变量 但地址值是一样的,说明,该地址绝对不是物理地址!
- 在Linux地址下,这种地址叫做虚拟地址。
- 我们在用C/C++语言所看到的地址,全部都是虚拟地址!物理地址,用户一概看不到,由OS统一管理。
(2)理解c/c++地址空间分布
(3)虚拟地址空间与物理地址空间
上面的图就足矣说名问题,同一个变量,地址相同,其实是虚拟地址相同,内容不同其实是被映射到了不同的物理地址!
14、进程创建
关于页表拷贝
在Linux系统下,当子进程通过fork()函数拷贝父进程的页表时,对于父进程中原本可读可写的页,其页表项在复制到子进程后通常会被设置为只读。这是Linux写时拷贝(Copy-On-Write, COW)机制的一部分。
具体来说,fork()函数在为子进程建立内核中的各种管理结构时,并没有立即为子进程分配真实的物理资源,而是将父进程的物理资源共享给子进程。这包括将父进程的物理内存(通过页表)共享给子进程。然而,由于父进程中的某些页原本是可读可写的,如果直接共享给子进程,那么任何一个进程对这些页内容的修改都会影响到另一个进程。
为了解决这个问题,Linux内核在将父进程的页表复制到子进程时,会将父进程中原本可写的物理页对应的页表项在父子进程的多级页表中都设置为只读。这样,当任何一个进程尝试对这些只读页进行写操作时,都会触发页错误(pagefault)。内核会识别出这种由写时拷贝机制导致的错误,并复制该物理页的内容到一个新的物理页,然后将新的物理页链接到发生写操作的进程的多级页表中,并恢复该物理页对应表项的写权限。
因此,在Linux下子进程拷贝父进程的页表时,对于原本可读可写的页,其权限确实会被设置为只读,以支持写时拷贝机制。这一机制有助于减少不必要的内存复制,提高系统效率。
15、进程终止
(1)进程退出场景
- 代码运行完毕,结果正确 。
- 代码运行完毕,结果不正确 。
- 代码异常终止。
(2)进程常见退出方法
正常终止(可以通过 echo $? 查看进程退出码):
- 从main返回。
- 调用exit。
- _exit。
异常终止- ctrl + c。
- 信号终止(如kill -9 结束程序)。
(3)_exit函数
#include <unistd.h>
void _exit(int status);
参数:status 定义了进程的终止状态,父进程通过wait来获取该值
(4)exit函数
#include <unistd.h>
void exit(int status);
exit最后也会调用exit, 但在调用exit之前,还做了其他工作:
- 执行用户通过 atexit或on_exit定义的清理函数。
- 关闭所有打开的流,所有的缓存数据均被写入。
- 调用_exit。
对比与_exit:
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<sys/types.h>
4 #include<stdlib.h>
5 int gval = 0;
6
7 int main()
8 {
9 printf("hehe");
10 //exit(1);
11 _exit(1);
12 return 0;
13 }
16、进程替换
(1)替换原理
用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。
(2)替换函数
#include <unistd.h>`
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[],
char *const envp[]);
//真正的系统调用
int execve(const char *path, char *const argv[], char *const envp[]);
函数参数
path : 程序文件路径。
arg… :命令行参数列表。
file :程序文件名。
envp[] : 环境变量。
argv[] :
用数组储存命令行参数,其中argv[0]为程序文件名。
函数解释
- 这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回。
- 如果调用出错则返回-1
- 所以exec函数只有出错的返回值而没有成功的返回值。
函数名exec后面字母的含义:
- l(list) : 表示参数采用列表
- v(vector) : 参数用数组
- p(path) : 有p自动搜索环境变量PATH
- e(env) :表示自己维护环境变量
使用
#include <unistd.h>
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);
}
具体例子
替换成
ls -a -l
1 #include<stdio.h>
2 #include<unistd.h>
3 #include <sys/wait.h>
4 #include<sys/types.h>
5
6
7 int main(int argc ,char * argv[])
8 {
9 pid_t pid = fork();
10
11 if(pid == 0)
12 {
13 execlp("ls","-a","-l",NULL);
14
15 }
16 else if(pid > 0)
17 {
18 pid_t rid = wait(NULL);
19 }
20 return 0;
21 }
运行结果: