进程
冯诺依曼式结构
我们现在常见的计算机以及公司里的服务器,大部分都遵守冯诺依曼体系结构:
五大硬件单元
冯·诺依曼体系结构是现代计算机的硬件体系结构,它包括五大硬件单元:输入单元,输出单元,存储器,运算器,控制器。
输入单元:键盘,网卡,磁盘,话筒
输出单元:显示器,网卡,磁盘,音响
存储器:一般都是物理内存
运算器:运算器是计算机中执行各种算术和逻辑运算操作的部件。
控制器:计算机控制器是计算机的神经中枢,指挥全机中各个部件自动协调工作。 在控制器的控制下,计算机能够自动按照程序设定的步骤进行一系列操作,以完成特定任务。
运算器控制器被集成在一个芯片中,该芯片就是:计算机最核心的部分CPU
主板:主板将相关组件关联起来
输入单元和输出单元统称为外设,计算机工作的流程大体上说可以是:数据必须被写入存储器,CPU通过控制器去内存拿数据,通过运算器运算后将结果返回给存储器,存储器再将数据刷新到输出设备
那么有一个问题:
可执行程序运行的时候,必须要先加载到内存,那么为什么CPU不直接从外设访问数据呢?
这里我们要看存储分级:
由存储分级图可知,离CPU越近速度越快,成本越高;离CPU越近速度越快,成本越低
所以为了平衡价格和速度,必须将数据通过输入单元加载到内存,CPU到内存拿数据进行运算,然后将结果返回给内存,内存在将数据刷新到输出设备,内存相当于是CPU和所有外设的缓存,在数据层面上,CPU不直接和外设打交道,CPU只和内存打交道,外设只和内存打交道
我们一起来看个问题:
你家在西安,你朋友家在贵州,你两分别在电脑上QQ上聊天,你给你朋友发了在吗?整个数据流向是什么样子的?
根据冯诺依曼结构,你的数据流向:键盘->存储器->CPU->存储器->网卡
通过网络到你的朋友那里,所以数据的输入设备是网卡/网络,所以到你朋友这里的数据流向是网卡->存储器->CPU->存储器->网卡(同时在QQ聊天界面上显示数据)
如果你给你朋友发送了一个word文档,那么数据流向的整个过程:
磁盘->存储器->CPU->存储器->网卡
网卡->存储器->CPU->存储器->磁盘
操作系统
概念
任何计算机系统都包含一个基本的程序集合,称为操作系统。操作系统包括
内核(进程管理,内存管理,文件管理,驱动管理)
其他程序(例如函数库,shell程序等等)
操作系统是什么?
它是进行软硬件资源管理的软件。硬件管理:网卡硬盘等;软件管理:进程管理,文件,驱动,以及卸载软件等
为什么要有操作系统呢?
操作系统层状结构
在底层硬件部分遵守冯诺依曼体系结构,驱动程序是执行者的角色。
操作系统不信任任何用户,不会有用户能够直接的对系统硬件或者软件的访问,任何对系统硬件或者软件访问,都必须通过某种操作系统提供的系统接口去访问接口,这些系统接口称为系统调用,系统调用可以简单理解为C语言接口,举个例子:
在银行整个系统当中,银行行长来管理所有的人工以及仓库、桌椅等等(相当于软硬件),当然银行也是不信任用户的,不可能让用户自己去仓库去取钱、存钱,此时就有了办业务的窗口,这就相当于是系统调用接口,用户可以通过系统调用接口来访问底层硬件,比如你是一位在银行取钱的80岁老人,在窗口进行办理业务,但是你什么也不懂,那么怎么办呢?银行会有一些人专门来帮助这些不懂的人,引领它们完成它们的业务。
用户直接使用系统调用接口(银行业务窗口)成本太高了,肯定需要懂一些系统的知识,所以就又有一层用户操作接口(办理业务的引领人)
总结:
计算机体系是一个层状结构,任何访问硬件或者系统软件的行为,都必须通过OS提供的系统接口
库函数接口:语言或者是第三方库给我们提供的接口,当我们程序中有相关的库函数的使用时此时使用systemcall,如果仅仅是计算则不使用systemcall
系统调用接口:OS提供的接口
理解"管理"
我们通过例子进行管理的理解:
比如在学校的封闭的管理中,学校里有:学生(被管理者)、辅导员(执行者),校长(管理者),
我们发现在自然界中的事情绕不开这两点:
1、做决策
2、做执行
在学校中,我们基本见不到校长,我们既定事实:管理者和被管理者并不见面。管理者和被管理者并不见面,那么校方是如何知道你的学校的学生呢?那是因为你的个人信息在学校的系统里,那么管理者怎么管理被管理者呢?本质是通过"数据"进行管理的,那么数据是怎么来的呢?数据是执行者进行录入。既然是管理数据,就一定先要把学生信息抽取出来,抽取管理数据的过程,我们可以称之为描述学生,在C语言代码中,可以定义struct结构体进行描述学生:struct student tom = {…};因为学生多,为了更好的管理对象,需要将这些对象组织起来,所以定义双向循环链表。这样学生管理工作,就转换成为了对双链表的增删查改操作。
总结:管理的本质是对被管理对象进行先描述,被管理对象一般是很多的,所以通过某种数据结构进行组织,对学生的管理转换成了对链表的管理
我们可以将这些对应起来:校长称为操作系统,辅导员称为驱动,学生称为软硬件
进程
什么是进程?
我们在写了一份代码时,将他编译链接成一个可执行程序时,这份可执行程序在磁盘中,运行它,此时就生成了一个进程。我们知道在运行这个程序时,首先这个程序会先加载到内存当中,那么程序加载到内存里就是进程吗?答案是并不是这么简单的。
操作系统可以一次跑起来多个程序,在操作系统中运行起来的程序有很多,那么OS需要管理起来这些运行起来的程序,如何管理进程?**先描述,再组织。**那么如何描述进程呢?
为了描述进程,操作系统提供了PCB(进程控制块:struct task_struct结构体),PCB里几乎包含进程相关的所有的属性信息
每一个进程都有自己的进程控制块,当这些进程控制块变得多起来的时候,操作系统就将它们组织起来:将进程控制块用双链表组织起来
所以操作系统对进程的管理转换成为了:对进程信息的管理,先描述再组织,对进程管理转换成了对双链表的增删查改!
为什么要存在PCB(task_struct)呢?
因为操作系统要对进程进行管理!先描述,再组织
所以得出结论:进程 = 你的程序 + (内核申请的数据结构)PCB
什么是PCB?
进程控制块,是用来描述进程的属性集合
PCB如何描述进程?
task_struct内容分类
标识符: 描述本进程的唯一标示符,用来区别其他进程。
ps ajx
ps ajx | head -1 && ps ajx | grep 'mytest'
这里的PID就是标识符
状态: 任务状态,退出代码,退出信号等。
比如我们早上八点在吃饭、凌晨2点在睡觉、晚上十点在打游戏,这就是状态
优先级: 相对于其他进程的优先级。
比如中午在排队买饭,一次只能打一份饭,排队靠前优先级高,靠后优先级低,优先级本质是在资源有限的前提下,确立谁先访问资源,谁后访问的问题
程序计数器: 程序中即将被执行的下一条指令的地址。
程序语言中:
- 顺序语句
- 判断语句
- 循环语句
当我们写了若干行代码时,正在执行一行代码,CPU执行完毕时,继续执行下一行,这叫顺序语句
CPU核心工作流程是:
- 取指令
- 分析指令
- 执行指令
CPU运行的代码都是该进程的代码。那么CPU如何得知应该取进程中的哪行指令?
在CPU中有个eip寄存器(PC指针),保存当前正在执行指令的下一条指令的地址
内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
通过内存指针,可以找到对应的进程对应的代码和数据
上下文数据: 进程执行时处理器的寄存器中的数据[休学例子,要加图CPU,寄存器]。
I/O状态信息: 包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。
记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。
还有其他信息
通过系统调用获取进程标示符
getpid:获得进程id
getppid:获得父进程id
#include<stdio.h>
int main()
{
while(1)
{
printf("i am a process,my pid is :%d;my father is :%d\n",getpid(),getppid());
}
return 0;
}
查看进程方式
ps命令
命令用于显示当前进程的状态,类似于 windows 的任务管理器。
ps ajx
/proc目录
这个目录下的以数字命名的文件就是进程
top命令
top命令也可以查看到进程
上下文概念
进程在运行的时候,会产生很多临时数据,这些CPU内的寄存器数据,称为进程的硬件上下文
时间片:进程放在CPU上之后,并不是一直在运行直到进程运行结束,理论上每个进程都有一个运行时间单位,称为时间片,时间到了就让该进程剥离CPU
一般进程让出CPU的情况有两种:
单CPU的电脑称为单核:跑起来多个进程,通过进程快速切换的方式,在一段时间内,让所有的进程代码都得到推进,并发!
多CPU的电脑称为多核:任何时刻,允许多个进程同时执行,并行!
进程在CPU上运行,会有很多寄存器上的临时数据,这些临时数据称为上下文数据
进程切换
我们通过一个例子来说明进程切换:比如你要休学,我们首先需要保留学籍,等休学结束,我们需要恢复学籍
同样的,当一个进程在运行中,因为某些原因,需要被暂时停止执行,让出CPU。此时需要进程保存自己所有的临时数据(当前进程的上下文数据),当然保存的目的是为了恢复。一个进程在运行中比如时间片到了,首先将临时数据保存在该进程的PCB中,然后让下一个进程使用CPU,该进程结束后,再继续下一个进程,如果该进程有临时数据,则将该临时数据恢复,继续它的任务。这就是进程切换
通过系统调用创建进程—fork
fork可以用来创建一个子进程,一个现有进程可以调用fork函数创建一个新进程。由fork创建的新进程被称为子进程(child process)。fork函数被调用一次但返回两次。两次返回的唯一区别是子进程中返回0值,而父进程中返回子进程ID
#include <stdio.h>
#include <unistd.h>
int main()
{
printf("i am father : %d\n",getpid());
fork();
while(1)
{
printf("i am a process!, pid: %d,ppid:%d\n",getpid(),getppid());
sleep(1);
}
}
我们可以看到此时有两个进程,pid为24543的进程的ppid为24544,pid为24544的即为父进程。可能有人会疑问父进程和子进程在fork之后谁先运行呢?答案是我们无法确定fork之后是子进程先运行还是父进程先运行,这依赖于系统的实现。
理解fork
父子进程共享用户代码,而用户数据各自私有一份
因为用户代码是只读的,不可以修改的,所以共享用户代码不影响,而为什么用户数据各自私有一份?
因为在操作系统中,所有进程具有独立性,这是操作系统表现出来的特性,如何做到进程互相干扰?就让用户数据各自私有一份,这样进程就不互相干扰了
fork创建子进程是不是系统多了一个进程?
答案是的确多了一个进程,进程 = 你的程序 + (内核申请的数据结构)PCB
创建子进程,那么该进程就需要有PCB,子进程的PCB是拷贝父进程的(PID和PPID不拷贝),通常以父进程为模板,其中子进程默认使用的是父进程的代码和数据(写时拷贝)
我们前面说了:fork函数被调用一次但返回两次。两次返回的唯一区别是子进程中返回0值,而父进程中返回子进程ID:
#include<stdio.h>
#include<unistd.h>
int main()
{
printf("i am father process : %d\n",getpid());
pid_t ret = fork();
if(ret == 0)
{
while(1)
{
printf("i am child process :,pid:%d,ppid:%d\n",getpid(),getppid());
}
}
else if(ret>0)
{
while(1)
{
printf("i am father process :,pid:%d,ppid:%d\n",getpid(),getppid());
}
}
return 0;
}
fork为什么会有两个返回值?
当fork函数在准备return返回时,子进程已经被创建成功了,子进程已经被放在了调用队列,等待返回。因为父进程和子进程共享代码,父进程有return代码,子进程也有return代码,因此fork函数会返回两次,一次是在父进程中返回,另一次是在子进程中返回,这两次的返回值是不一样的。
为什么给子进程返回0,给父进程返回子进程的pid?
自然世界中,任何一个儿子只有一个父亲,一个父亲可以有多个子女,儿子找父亲是特别简单的,父亲为了更好的使用孩子,给每个孩子有标识,并且记住它
进程状态
Linux操作系统下的进程状态
一个进程可以有几个状态,在Linux内核源代码中有一个任务状态数组task_state_array,下面的状态在内核源代码中定义:
R(running):运行状态
S(sleeping):睡眠状态
D(disk sleep):磁盘休眠状态
T(tracing stop):停止状态
Z(zomble):僵尸状态
X(dead):死亡状态
R状态
进程是R状态,那么它一定在CPU上面运行吗?
不一定!进程在运行队列中,就叫做R状态
R状态表示的是,我准备好了,可以被调度
S状态
#include <stdio.h>
#include <unistd.h>
int main()
{
printf("i am father : %d\n",getpid());
fork();
while(1)
{
printf("i am a process!, pid: %d,ppid:%d\n",getpid(),getppid());
sleep(1);
}
}
我们写了一个死循环的代码,./mytest,它一直在运行没有停止,那么它的状态应该是R,为什么它的状态是S呢?
是因为程序的大部分时间都在睡眠,里面有一句代码sleep(1);程序大部分情况是在睡眠,所以它是S状态
S是休眠状态(浅度休眠,大部分情况是在睡眠),可以被唤醒
处于这个状态的进程因为等待某某事件的发生(比如等待socket连接、等待信号量),而被挂起。这些进程的task_struct结构(进程控制块)被放入对应事件的等待队列中。当这些事件发生时(由外部中断触发、或由其他进程触发),对应的等待队列中的一个或多个进程将被唤醒。
D状态
Disk Sleep(深度休眠)
我们将进程数据写入磁盘,是要花时间的,进程就在等待磁盘写入的状态(成功还是失败)
在等待期间,操作系统看到它在休眠,不干事情还能让你安静的呆着?于是进程就被操作系统干掉,那么过一会磁盘说它写入失败了,此时该进程也被干掉了,磁盘不知道将该数据给谁,那么这部分数据该怎么办呢?这部分数据就被丢了。
这里好像谁都没有错,错的根源是操作系统可以干掉进程,让操作系统干不掉该进程就可以了,所以就出现了D状态(深度休眠状态):深度休眠的状态不可以被杀掉,即便是操作系统也不可以,D状态一般是在将数据写入磁盘时设置的,只能等待D状态进程自动醒来,或者是关机重启
T状态
T停止状态(stopped): 可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可 以通过发送 SIGCONT 信号让进程继续运行。
int main()
{
while(1)
{
printf("hello world\n");
sleep(1);
}
}
我们写一个程序让他运行起来,然后kill -19 pid,这样就可以将该进程停止,kill命令用来删除执行中的程序或工作。-19是kill的选项,是SIGSTOP信号,用于将进程停止
我们选择kill-18选项就可以将该进程继续:
僵尸进程
僵尸进程也就是Z状态:
进程退出,一般不是直接就让OS回收资源,释放进程的所有资源,创建进程的目的是为了完成某件任务或者工作,判断进程是都正常退出,如果是正常退出,判断是否完成某件任务或者工作
进程退出的时候,会自动将自己退出时的相关信息,写入进程的PCB中,供OS或者父进程进行读取!
进程退出,OS或者父进程并没有使用wait()或者waitpid()对它们进行状态收集读取它的退出信息时的状态称为僵尸状态
读取成功之后,该进程才算真正死亡,这种状态称为X
#include<stdio.h>
#include<unistd.h>
int main()
{
printf("i am father process : %d\n",getpid());
pid_t ret = fork();
if(ret == 0)
{
while(1)
{
printf("i am child process :,pid:%d,ppid:%d\n",getpid(),getppid());
sleep(5);
exit(1);
}
}
else if(ret>0)
{
while(1)
{
printf("i am father process :,pid:%d,ppid:%d\n",getpid(),getppid());
sleep(30);
}
}
return 0;
}
我们将子进程先退出,让父进程进入休眠状态,可以看到子进程在这期间是僵尸进程:
此时Linux下的进程状态就可以和最前面的那幅图对应起来了,就绪相当于R状态,执行也相当于是R状态,阻塞相当于是S、D、T状态,退出相当于是X状态:
孤儿进程
有一个问题:父进程如果提前退出,那么子进程在退出时进入了Z状态,那么它的父进程已经退出了,那么此时要怎么处理呢?
int main()
{
printf("i am father process : %d\n",getpid());
pid_t ret = fork();
if(ret == 0)
{
while(1)
{
printf("i am child process :,pid:%d,ppid:%d\n",getpid(),getppid());
sleep(1);
}
}
else if(ret>0)
{
while(1)
{
printf("i am father process :,pid:%d,ppid:%d\n",getpid(),getppid());
sleep(1);
exit(1);
}
}
return 0;
}
我们上面代码将父进程退出了,此时子进程的PPID就变成了1,PPID为1的进程其实就是操作系统,所以当父进程退出,子进程会被操作系统领养
需要注意的是:
如果一个进程是Z状态(僵尸进程)或者D状态(深度睡眠),kill是杀不掉的
进程的优先级
优先级和权限有什么区别?
二者有很大的区别,优先级是一定能得到某种资源,只不过是得到的快与慢,权限是决定你能还是不能得到某种资源
优先级是得到某种资源的先后顺序,为什么要有优先级?本质是因为资源有限
Linux的优先级是由PRI和NI值共同确定。
可以根据ps-l进行当前进程运行状态以及优先级相关概念的查询:
ps -l
优先级的数字越小,优先级越高;优先级的数字越大,优先级越低,优先级能设置,但是优先级不可能一味的高,也不可能一味的低,OS的调度器也要适度的考虑公平问题,避免"饥饿问题",NI叫做nice值,nice值就是优先级的修正数据:[-20,19]
我们可以看到其中的进程的相关信息:
UID : 代表执行者的身份 PID : 代表这个进程的代号 PPID :代表这个进程是由哪个进程发展衍生而来的,亦即父进程的代号 PRI :代表这个进程可被执行的优先级,其值越小越早被执行 NI :代表这个进程的nice值
uid是什么呢?
在Linux系统中,标识一个用户,并不是通过用户名标识,因为用户名是可以修改的。在Linux当中,而是通过用户的uid来标识用户的,Linux当中的用户名就相当于我们qq的昵称,uid就相当于qq号码,为什么uid是数字呢?因为计算机比较善于处理数据
PRI and NI
- PRI,即进程的优先级,或者通俗点说就是程序被CPU执行的先后顺序,此值越小,进程的优先级别越高
- NI就是我们所要说的nice值了,其表示进程可被执行的优先级的修正数值
- PRI值越小越快被执行,那么加入nice值后,将会使得PRI变为:PRI(new)=PRI(old)+nice
- 这样,当nice值为负值的时候,那么该程序将会优先级值将变小,即其优先级会变高,则其越快被执行
- 所以,调整进程优先级,在Linux下,就是调整进程nice值
- nice其取值范围是-20至19,一共40个级别。
需要注意的是:进程的nice值不是进程的优先级,它们不是一个概念,但是进程nice值会影响到进程的优先级:PRI(new)=PRI(old)+nice
怎么调整优先级?
我们首先写一个程序让这个进程跑起来:
可以看到默认的PRI值为80
此时我们输入top命令,进入top命令并按R:
此时你需要输入一个PID,比如我们想要调整proc的优先级,我们就将它的PID输进去,输入之后会出现这个:
这里是设置你的nice值,通过nice值来调整PRI,比如我们输入10:
可以看到proc进程的PRI已经修改成功了。
那么我们想再次修改呢?
修改之后发现PRI是75,不应该是65吗?为什么呢?
PRI(new)=PRI(old)+nice,是因为PRI(old)值默认都为80,他不会因为你修改了优先级,就更新PRI(old)值
原因是有一个基准值方便调整,在设计上,实现比较简单,如果默认不设置成80,那么这里就会出现一个bug,你的进程的优先级可以修改的无限高,我们知道nice值是有范围的:[-20,19],如果old值不默认,我们每次调整优先级都将nice值设置成-20到0之间,优先级就会无限高,而且这里nice值也是合法的,所以就产生了bug。
操作系统内的调度器要“公平”且较高效的调度。公平的概念:公平不是平均,是尽量的让每个进程更好的进行调度。
进程的其他概念
并行:多个进程在多个CPU下分别,同时进行运行,这称之为并行,公司里的服务器一般有多个CPU
并发:多个进程在一个CPU下采用进程切换的方式,在一段时间之内,让多个进程都得以推进,称之为并发。我们一般人用的计算机一般都是一个CPU
独立性:多进程运行,需要独享各种资源,多进程运行期间互不干扰,这也是操作系统设计进程的一个原则
#include<stdio.h>
#include<unistd.h>
int main()
{
pid_t id = fork();
if(id==0)
{
sleep(5);
int a =1/0;
}
else
{
while(1)
{
printf("father:i am alive\n");
}
}
return 0;
}
我们写一个程序,让子进程退出,我们发现父进程还在运行,说明进程具有独立性
竞争性:系统进程数目众多,而CPU资源只有少量,甚至1个,所以进程之间是具有竞争属性的,为了高效完成任务,更合理竞争资源,便有了优先级
自然世界里面有没有竞争呢??肯定有,为何会有竞争呢?人多,资源少(岗位,经济问题),那么怎么证明你是竞争的胜利者呢?排名靠前,确立优先级,也就是优先级较高
进程和资源之间,进程永远是多数,所以进程之间存在竞争性。
环境变量
基本概念
Linux中的特殊的系统级变量,环境变量(environment variables)一般是指在操作系统中用来指定操作系统运行环境的一些参数 如:我们在编写C/C++代码的时候,在链接的时候,从来不知道我们的所链接的动态静态库在哪里,但是照样可以链接成功,生成可执行程序,原因就是有相关环境变量帮助编译器进行查找。 环境变量通常具有某些特殊用途,还有在系统当中通常具有全局特性
常见环境变量
echo $PATH
查看环境变量,$开头表示变量
env
可以显示当前用户的环境变量
下面我们说明一下PATH环境变量的作用:
int main()
{
printf("i am a commond\n");
return 0;
}
有一个问题:生成的可执行程序是不是一个命令?
答案是它是命令,程序,命令,指令,可执行程序…都是一个概念,它们都是命令
那么既然它是命令,我们这样运行它,为什么它说命令找不到呢?
我们./proc就可以运行,但是在Linux当中运行ls、pwd等命令都不要./,那么我们自己写的代码生成可执行程序为什么要用./呢?
./表示的是当前路径,报错找不到,说明系统曾经找过,但是没找到,在Linux系统中,我们输入一条命令
系统会在PATH里面找,PATH的作用就是辅助系统进行指令查找,因为当前可执行程序没在PATH里所以找不到:
PATH的内容中:作为分隔符,每一个路径称为绝对路径,当输入一个命令时,分别在每个绝对路径里找
那么我们怎样就可以不用./就可以运行命令呢?
cp proc /usr/bin
可以看到直接输入proc就可以运行了
PATH=$PATH:/home/zsb/11-26
注意
P
A
T
H
的
意
思
是
在
原
有
的
路
径
上
添
加
路
径
,
如
果
不
写
PATH的意思是在原有的路径上添加路径,如果不写
PATH的意思是在原有的路径上添加路径,如果不写PATH,就会将PATH的内容全部只赋值成该内容,只有下一次重新登录时才会恢复
可以看到当前路径已经在PATH里面了
echo $HOME
普通用户的默认登录的主工作目录是:/home/用户名,root用户的主工作目录是:/root,不同的用户为什么所处的默认路径不一样呢?
本质上是因为$HOME的内容决定的
echo $SHELL
命令行解释器的种类
echo $HISTSIZE
和环境变量相关的命令
echo:显示某个环境变量值
export:设置一个新的环境变量
env:显示所有的环境变量
unset:清除环境变量
set:显示本地定义的shell变量和环境变量(这两个变量的区别下面会讲)
通过系统调用获取环境变量
getenv
#include<stdlib.h>
int main()
{
printf("%s\n".getenv("PATH"));
printf("%s\n".getenv("HOME"));
printf("%s\n".getenv("SHELL"));
return 0;
}
环境变量通常是具有全局属性的
首先我们来看一个概念:
#include<stdio.h>
#include<stdlib.h>
int main()
{
printf("i am a proc : pid:%d,ppid:%d\n",getpid(),getppid());
printf("%s\n",getenv("PATH"));
printf("%s\n",getenv("HOME"));
printf("%s\n",getenv("SHELL"));
return 0;
}
bash的PID为32651,而proc进程的PPID就是32651,在命令行上运行的大部分的指令,它的父进程都是bash,bash创建子进程,子进程执行你的命令
而在命令行中,我们可以定义两种变量:
属性:只能在当前shell命令行解释器内被访问,不可以被子进程继承,没有全局属性
创建本地变量:
MY_VAL = "you can see me"
查看本地变量的内容:
echo $MY_VAL
怎么证明本地变量的这个只能在当前shell命令行解释器内被访问,不可以被子进程继承这样的属性呢?
#include<stdio.h>
#include<stdlib.h>
int main()
{
printf("%s\n",getenv("MY_VAL"));
printf("%s\n",getenv("HOME"));
printf("%s\n",getenv("SHELL"));
return 0;
}
可以看到此时程序发生了段错误
有人可能眼睛敏锐,想到前面我们说了在命令行上运行的大部分的指令,它的父进程都是bash,bash创建子进程,子进程执行你的命令,那么echo也是命令呀,按道理使用echo命令也会创建子进程来执行命令,前面我们又说了本地变量不可以被子进程继承,但是刚刚我们用echo命令访问了本地变量,那么本地echo命令为什么就可以访问本地变量呢?
在命令行上运行的大部分的指令,它的父进程都是bash!bash创建子进程,子进程执行你的命令,注意看关键字眼,我说的是大部分指令它们的父进程都是bash,而echo是内建命令:它是shell程序内的一个函数。所以它当然可以访问了。
下面我们来看怎么查看本地变量?
这样可以吗?
env | grep MY_VAL
env用来显示所有环境变量,这里用env显示,发现没有MY_VAL,更加证明它是本地变量
那么怎么查看本地变量呢?这样查看:
set | grep MY_VAL
set既可以显示本地变量,也可以查环境变量
export MY_VAL
将MY_VAL导成环境变量,环境变量具有"全局属性":可以被子进程继承
当我们讲MY_VAL导成环境变量时,我们刚刚的那个程序就可以访问它了:
环境变量的组织方式
main函数的参数argc和argv
深入学习过C语言的同学会知道:main函数其实是可以带参数的
int main(int argc,char *argv[])
argv是字符指针数组,argc是统计argv数组中的元素个数
那么这两个参数到底是什么呢?我们来打印一下:
int main(int argc,char *argv[])
{
for(int i =0;i<argc;i++)
{
printf("argv[%d]:%s\n",i,argv[i]);
}
}
图解:
其中在这个例子当中argc就是4,argv是一个字符指针数组,argv[0]指针指向字符串"./proc",argv[1]指向字符串"1",argv[1]指向字符串"2",argv[1]指向字符串"3",这两个参数叫做命令行参数
那么为什么要存在命令行参数?看下面的一个例子
#include<stdio.h>
#include<stdlib.h>
int main(int argc,char *argv[])
{
if(argc!=4)
{
printf("格式输入错误,正确格式如下:\n");
printf("%s -[a|s] x y\n",argv[0]);
return 1;
}
int x = atoi(argv[2]);
int y = atoi(argv[3]);
int z = 0;
if(strcmp(argv[1],"-a")==0)
{
printf("%d + %d = %d\n",x,y,x+y);
}
else if(strcmp(argv[1],"-s")==0)
{
printf("%d - %d = %d\n",x,y,x-y);
}
else
{
printf("格式输入错误,正确格式如下:\n");
printf("%s -[a|s] x y\n",argv[0]);
}
return 0;
}
写出上面的程序后,我们这样运行,显示格式错误:
我们输入正确的格式:
可以看到这样的命令行参数帮助我们设计出同一个程序可以实现出不同的业务功能:加法和减法
我们仔细想想,这不就是我们Linux当中命令的选项吗?ls等命令带有众多选项就是这样实现的。
main函数的第三个参数env
*main函数还有第三个参数:char env[],它是一个指针数组,它指向环境变量
我们将他打印一下:
int main(int argc,char *argv[],char*env[])
{
int i = 0;
for(i =0;env[i];i++)
{
printf("%s\n",env[i]);
}
}
env的元素指向一个一个环境变量字符串,所以可以以数组传参的方式,把环境变量传递给当前的程序,当前程序运行起来也就是进程,当前进程拿到了环境变量,就可以打印
proc这个进程是bash的子进程,proc这个进程可以访问环境变量是因为环境变量完成了继承,环境变量的继承是通过第三个参数来完成的,实现环境变量被子进程继承下去,从而实现全局变量的“全局属性”。
访问环境变量的方式
int main(int argc,char *argv[],char*env[])
{
int i = 0;
for(i =0;env[i];i++)
{
printf("%s\n",env[i]);
}
}
#include<stdio.h>
#include<stdlib.h>
int main()
{
printf("%s\n",getenv("PATH"));
printf("%s\n",getenv("HOME"));
printf("%s\n",getenv("SHELL"));
return 0;
}
#include<stdio.h>
int main()
{
extern char **environ;
for(int i =0;environ[i];i++)
{
printf("%s\n",environ[i]);
}
}
environ这个二级指针指向env,以此来访问环境变量
我们写这样一个程序来帮助上面的理解:
void show()
{
printf("hello show!\n");
}
int main()
{
show(10,20);
return 0;
}
这段程序会报错吗?答案是并不会报错:
为什么呢?10和20传去哪里了呢?我们并不能在函数里面使用10和20,那么为什么不会报错呢?是因为函数调用会建立栈帧的,10和20被压入栈帧,在这个show函数中,我们可以通过某些指针来操作10和20,相应的,environ就类似于这样的指针,我们main函数并没有写参数,但是不会报错,可以访问环境变量
|