- 输入设备:键盘,鼠标,摄像头,麦克风,网卡,硬盘。
程序要运行起来需要把程序加载到内存中去,而没加载的时候存储在硬盘中,所以硬盘此时是输入设备
输出设备:显示器,声卡,喇叭,硬盘…。
输出设备给的也不一定是给人的,也可以是给网络的。
输入输出设备(外围设备,外设)【除了内存和cpu其他都可以认为是外设】
内存:掉电易失性存储介质。存储速度比较快(相对硬盘等其他设备)。
- CPU ——寄存器 纳秒
- 内存——微秒
- 硬盘,SSD,FLASH——毫秒
- 光盘
- 磁带
- 距离CPU越近,存储效率越高,单价成本越高且一般是比较小的。
- 存储分级能用最小的成本使用上高效率的计算机,我们现在用的计算机,处于考虑成本和效率的结果。
- 同时是快的给慢的做缓存。比如之前的缓冲区,要打印到屏幕上可以将其视为外设,外设离cpu远,内存离cpu近。所以我们把输出先放内存,由内存做缓存区。
- 为何输入/输出设备不直接给CPU反而给内存?
- 离cpu越近的效率越高,离cpu越远的效率越高。外设比较慢,cpu比较快,因此为了整体的效率,在外设和cpu之间加了内存。可以认为内存整体是外设和cpu的“缓存”!
- 为什么程序运行的时候要先加载进内存?
- 程序在硬盘上如果不加载那么cpu不断访问外设效率很低。加载进内存相当于预加载,把程序缓存进来了。
运算器:算术和逻辑运算
控制器:什么时候把数据送到运算器,什么时候送到外设,中断等非数据性的,数据控制逻辑
预加载:提前把输入设备数据加载到存储器(有些设备的数据可以预加载,比如所有程序运行之前要加载到内存中,加载代码的时候先加载一大部分。再比如看电影时的预加载。键盘的实时读入是监听程序,监听完再统一输入)
预写入:把cpu处理完来不及给输出设备的先写到存储器。
不管如何我们可以把要写入的和要写出的都放存储器然后定期刷新。
这个工作是操作系统做的。
最后从两条信号数据线看出:
站在数据层面上,cpu不和外设打交道,直接和内存打交道
外设角度,数据不和cpu打交道,直接和内存打交道
内存:计算机数据的核心。
举例:平时的网络聊天,从冯诺依曼角度分析下数据是怎么从我们电脑到朋友的电脑上的
键盘输入->内存->运算器(封装成报)->内存->外设(显示器,网卡),输入(网卡)->内存->运算器(解包)->内存->输出设备(显示器)
关于冯诺依曼,必须强调几点:
- 这里的存储器指的是内存
- 不考虑缓存情况,这里的CPU能且只能对内存进行读写,不能访问外设(输入或输出设备)
- 外设(输入或输出设备)要输入或者输出数据,也只能写入内存或者从内存中读取。
- 一句话,所有设备都只能直接和内存打交道
同理,操作系统也是软件。那么软件要运行就要加载到内存,所以操作系统也是在内存里的。
操作系统:做管理工作的,做软硬件管理(硬件:冯诺依曼中的所有设备。软件:应用层面:1.安装软件,卸载 2.在系统层面,文件,进程,驱动)
用银行系统来举例。
首先必须要有硬件设施才可能出软件逻辑。比如学校要先有宿舍,教室等才能开学校。
驱动程序相当于宿管,核心工作是把上层给的管理任务传递给下层。比如疫情要提前放寒假,宿管通知学生信息并且清除顽固分子。
驱动程序是软件,有多少种硬件就有多少种驱动,一旦多了就要被管理起来。
管理:真正的管理是要有“决策权”
区分“决策”和“执行”。比如宿管实际上只是一个执行者,能让离校的是学校领导。同时要注意并不是说决策者不做执行,并不是说所有的执行者不做决策。操作系统大多时候在决策,驱动等大部分在做执行。
在学校当中,底层硬件部分软件:学生(被管理者)。
操作系统:管理者,有真正的决策权力。
驱动:导员,楼管阿姨,图书管理员。
- 管理者和被管理者从来都没有见过面,如何进行管理?
- 可以通过“信息”管理
- 如何获得信息呢?
- 驱动层可以帮管理员拿到数据。
- 比如一个学校的学生成绩信息,毫无疑问很多。
- 当“信息量”特别大的时候,信息就需要被组织起来了。
- 一万个学生,每个学生的属性是类似的,先把角色描述起来!
- struct Student{姓名,电话,各科成绩,特长}。Student stu1,stu2,stu3…
- struct{base,struct Stu* next}
因此如何进行管理:先描述,再组织。
因此对抽象事物的管理就变成了对数据结构的管理。
因此现在回答了管理是什么的问题和怎么办管理的问题。
回到操作系统,操作系统要进行各种管理。操作系统如何进行硬件管理?操作系统启动的时候,插入鼠标的时候等已经把对应的设备先描述再组织起来了。操作系统通过驱动程序获得底层硬件的数据信息供操作系统做决策使用。
对于内存管理,操作系统把内存看作一块大数组。
对于进程管理,操作系统先描述进程再管理,一般把所有进程通过链表管理起来。
对于文件管理,文件管理也是链表,不过稍微复杂点,涉及到内存级和外设级管理。
因此记住“先描述再组织”。
OS的使用是有成本的
OS一般是封装的,OS会提供一些接口。
正如银行系统对外提供服务是通过窗口提供的。操作系统对外提供的接口是系统调用接口。一般而言操作系统是管理者不和硬件直接打交道。作为用户的我们经常操控硬件,比如touch ,mkdir,创建文件的本质是在硬盘上创建了文件,用户的行为是会触发硬件访问的。再比如printf,在显示器(硬件)上打出消息。以及网上聊天,消息要传到网卡。
换言之,用户有访问硬件的需求,但用户不能直接访问硬件。用户一般不是直接访问硬件,用户需要穿过操作系统让操作系统完成这件任务。操作系统开一个用户进程来处理。
同时之前了解到,直接使用OS的成本是很高的。总不能让用户自己手动创建进程。同时用户有误操作直接使用os是有风险的。
因此操作系统封装起来对外提供一定的接口。所以用户访问硬件只要调用操作系统提供的接口即可。同时要注意到,我们去使用好系统调用,要知道参数,返回等。也是需要一定的背景知识和成本。
所以shell外壳帮助我们接受用户的命令交给操作系统。基本上可以理解为shell是基于系统调用之上做的又一层封装。
当然不是不是所有的库函数都包含系统调用。比如IO,网络的,系统级别的库函数会使用系统调用。比如最经典的printf就是库函数,底层访问了显示器的硬件。(实际上printf调用的系统调用函数是write())。
有了库函数之后我们只要关注打印什么即可,不用关注什么时候刷新缓冲区等。
小结:
- 库函数vs系统调用:具有上下级的关系,库函数(用户级别的库)可能调用系统调用。
- 当程序从硬盘中读入进来到内存中就被称为进程
- 系统允许多个进程同时运行,OS需要对进程进行管理,如何管理进程?
- 描述进程的结构体叫PCB(Process Ctrl Block)
- linux底下为的PCB叫struct task_struct
- 目前的进程概念:可执行程序与管理进程需要的数据结构的集合。
ps aux | head -1 && ps aux | grep test.out #查看进程
放到后台ctrl+c无法停止。
./xxx &
killall 进程名 #杀掉进程
kill -9 进程名 #杀掉进程
先描述,再组织!用结构体(PCB-Process Ctrl Block)描述然后用链表组织。进程管理转化成了对链表的管理。
在linux中具体结构体为struct task_struct{};
struct task_struct vs PCB:王婆 和 媒婆的关系
PCB在任何os书上都叫pcb,但具体一块操作系统的名字是不同的。
正如之前的文件=内容+属性。进程=对应的文件+进程属性。进程比文件本身要大。
因此载入内存的进程结构很多,操作系统内部就有对应的结构体。
描述进程的数据结构,就是一批结构体对象。
现在学到的进程:可执行程序与管理进程需要的数据结构的集合。
在Linux中描述进程的结构体叫做task_struct。 task_struct是Linux内核的一种数据结构,它会被装载到RAM(内存)里并且包含着进程的信息。
tast_struct的元素:
- 标示符: 描述本进程的唯一标示符,用来区别其他进程。
- 状态: 任务状态,退出代码,退出信号等。
- 优先级: 相对于其他进程的优先级。
- 程序计数器: 程序中即将被执行的下一条指令的地址。
- 内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
- 上下文数据: 进程执行时处理器的寄存器中的数据[休学例子,要加图CPU,寄存器]。
- I/O状态信息: 包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。
- 记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。
可以在内核源代码里找到它。所有运行在系统里的进程都以task_struct链表的形式存在内核里。
ls /proc/1
大多数进程信息同样可以使用top和ps这些用户级工具来获取
ps aux | grep 进程名
ps aux | head -1 && ps aux | grep 进程名
man ps
#include
#include
#include
using namespace std;
int main(){
while(1){
cout<<"hello process:"<<getpid()<<" "<<getppid()<<endl;
sleep(1);
}
}
仔细看这里的父进程是bash。我们将当前进程终止一下再执行,发现父进程是没有变的。
继续上次的事,张三屡次找王婆说媒不成,但死不放弃。王婆一方面由于张三的爹是村长,不能拒绝,另一方面知道这个事成不了屡次说不成败坏自己名声。于是王婆开一个婚介所自己当ceo,找了一个实习生去干这个事。这就是创建子进程完成基本命令。
也就是说这里的bash(shell的一种)就是王婆,bash在linux中也是一个进程,但这个进程不能挂。挂了其他的linux操作就不能操作了。
但是怎么保证bash不挂呢?因为总有些命令会失败。所以bash找了一个实习生(也就是创建子进程),把任务交给子进程。进程是具有独立性的。bash只要不挂,就能继续接其他的任务。
bash运行原理:bash叫做命令行解释器,通常是如何解释的呢?通过创建子进程,让子进程去完成对应的任务。那么bash的任务就是接收任务,创建子进程。
那么有没有一些事必须要bash去亲自做呢?答案就是有十足把握的事。之后提及。
#include
#include
int main(){
printf("输入一次:\n");
fork();
sleep(1);
printf("输出两次 pid:%d ppid:%d\n",getpid(),getppid());
sleep(1);
return 0;
}
#include
#include
int main(){
printf("输入一次:\n");
int ret=fork();
if(ret>0){
//parent
printf("I am father! pid is:%d\n", getpid());
}
else if(ret==0){
//child
printf("I am child! pid is:%d\n",getpid());
}
else {
printf("fork error!\n");
}
sleep(1);
return 0;
}
发现if和else的两个逻辑都运行起来了。按照C、Cpp的部分if和else if是不能同时进行的。但是在系统中有两个执行流,同时跑两个。
创建进程,是系统多了一个进程,多了一个进程,系统就要多一组管理进程的数据结构+该进程对应的代码和数据。最典型的就是进程控制块pcb。
fork()是函数吗?是
pid_t fork(){
//创建子进程的逻辑
//给子进程创建task_struct
struct task_struct * ts=(struct task_struct*)malloc(sizeof(struct task_struct));
ts.XX=father.XX;
..
ts.status=runnning;
ts.link=task_queue;
//创建子进程的任务完了没
//子进程一旦创建完了,意味着子进程可以调度了,有两个进程执行流
return id;//return 也是语句,父进程要执行,子进程也会执行。
//函数的返回值,是数据,各自私有一份。虽然名字一样,对应的内存地址不一样。
//其实就相当于链表,进程形成了链表,父进程的fork函数返回的值指向子进程的进程id, 因为子进程没有子进程,所以其fork函数返回的值为0 .
}
子进程一旦创建完了,意味着子进程可以调度了,有两个进程执行流
return 也是语句,父进程要执行,子进程也会执行。函数的返回值,是数据,需要各自私有一份。虽然名字一样,对应的内存地址不一样。
为何给父进程返回子进程pid,给子进程返回0
进程数据=代码+数据。
父进程创建子进程的时候,代码是共享的,数据是各自私有一份(写时拷贝)
子进程代码是从fork处开始执行的, 为什么不是从#include处开始复制代码的?这是因为fork是把 进程当前的情况拷贝一份, 执行fork时,进程已经执行完了前面的代码。fork只拷贝下一个要执行的代码到新的进程。
代码是逻辑,一般是不可被修改的。
数据,即可读又可以写。
之前讲过,进程是具有独立性的。通过数据私有,表现出进程独立性!
父子进程fork完毕,谁先运行?不确定。这个由调度器决定。
如果单纯复制和父进程一样的代码,两个进程运行一样的逻辑没有什么意义。所以一般通过if和else来进行分流。
操作系统教材讲的理论都是总结性的,而具体操作系统实现会有所不同。
比如说就绪状态,阻塞状态在linux下都是咋样的。
为了弄明白正在运行的进程是什么意思,我们需要知道进程的不同状态。一个进程可以有几个状态(在 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 */
};
这里正在运行的进程却显示的是S,Sleep(),因为我们是将输出打到屏幕上,IO的速度比cpu慢,因此该进程大部分时间都是在Sleep等待的状态。后面的+表示的是这个进程运行在前台,运行到后台要取消的话就要杀掉进程。
后台运行的程序是没有+号的。
杀掉进程
killall 进程名 #杀掉进程 kill -9 进程名 #杀掉进程
那么我们把死循环改成空的不进行IO,那么该进程就是显示为R。
#include
#include
int main(){
while(1){
}
return 0;
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-w8wA9gi8-1638085342354)(3.linux进程.assets/image-20211124135801566.png)]
答案是不一定!
R状态实际表示的是该进程表示可以被调度,在运行队列中。
一旦是S状态,当事件就绪时,该进程立即被唤醒。系统当中可以有多个进程都在进行等待。所以系统中除了运行队列还有等待队列。
S也是睡眠状态,能被唤醒。可以理解为浅度睡眠。这个状态的进程是可以被操作系统给kill的。
D也是睡眠状态,不能被唤醒。可以理解为深度睡眠。经常在硬盘中用到。task_struct D保证进程无法被杀掉。
当读取数据的进程访问硬盘时,硬盘此时在找数据或者准备拷贝数据,而此时系统内存紧张了可能会崩,操作系统就要把一些进程给杀掉了。于是把读取数据(进程此时处于休眠等待的状态)给杀掉了。
于是硬盘找到的数据或者在拷贝的数据的接头人就没了。硬盘于是不知道怎么处理了。
为了保证这种不可知的状态发生,设置了D状态保证OS不杀。(除非强行重启或者IO完成)
于是就理解了一些高峰期的时候为什么服务器会挂掉。因为人流量高了就有大量的IO,系统的cpu不够,内存资源也不足,大量进程处于IO状态(D时间就久了)。长时间的只进不出服务器就挂掉了。
kill -19 xxxid #使进程停止
kill -18 xxxid #使进程继续
死亡状态,我们一般看不到,OS会回收。
- 僵死状态(Zombies)是一个比较特殊的状态。当进程退出并且父进程(使用wait()系统调用,后面讲) 没有读取到子进程退出的返回代码时就会产生僵死(尸)进程
- 僵死进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码。
- 所以,只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程进入Z状态
检测进程的脚本
while : ; do ps axj | head -1 && ps axj |grep proc |grep -v grep; sleep 1; echo '################'; done
#include
#include
#include
int main(){
printf("输入一次:\n");
int ret=fork();
if(ret>0){
//parent
while(1){
printf("I am father! pid is:%d\n", getpid());
sleep(1);
}
}
else if(ret==0){
//child
int count=0;
while(count<5){
printf("I am child! pid is:%d ppid is:%d\n",getpid(),getppid());
sleep(1);
count++;
}
exit(0);
}
else {
printf("fork error!\n");
}
sleep(1);
return 0;
}
现象:
来个故事梗概:一个程序员在你面前猝死了。打完120后没救了打110,警察来了后封锁现场,采集信息(如何死亡的问题),解除封锁,抬走尸体,通知家属。
请问这个程序员死了吗?死了。在世界上消失了吗?没有,尸体还在呢。为什么不让他消失呢?因为要确认死亡情况。
这里的父进程就是警察,父进程(系统调用,OS,检测进程运行完的时候,结果情况【1.是否正常运行完 2.是否异常 3.发生了什么异常 】)
保持进程基本退出信息,方便父进程读取,获得退出原因。
一般,僵尸进程的时候,task_struct是会被保留的,进程的退出信息是放在进程控制块中的!
所以,只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程进入Z状态。
直到父进程读取子进程状态之后子进程才会进入X状态。
wait和waitpid
退出信息在哪
样例
- 进程的退出状态必须被维持下去,因为他要告诉关心它的进程(父进程),你交给我的任务,我办的怎么样了。可父进程如果一直不读取,那子进程就一直处于Z状态?是的!
- 维护退出状态本身就是要用数据维护,也属于进程基本信息,所以保存在task_struct(PCB)中,换句话 说,Z状态一直不退出,PCB一直都要维护?是的!
- 那一个父进程创建了很多子进程,就是不回收,是不是就会造成内存资源的浪费?是的!因为数据结构对象本身就要占用内存,想想C中定义一个结构体变量(对象),是要在内存的某个位置进行开辟空 间!
- 内存泄漏?是的!
- 如何避免?后面讲
ps aux /ps axj #查看进程状态,后者能看到当前进程的进程状态
kill -l
1~31叫做普通信号。
后31个信号叫做实时信号。
- 父进程如果提前退出,那么子进程后退出,进入Z之后,那该如何处理呢?
- 父进程先退出,子进程就称之为“孤儿进程”
- 孤儿进程被1号init进程领养,当然要有init进程回收喽。
如果进程没有父进程,但是当前进程退出,进入僵尸,该进程的资源没有办法回收了,内存泄漏。
OS考虑了这个问题,孤儿进程是要被领养的,被OS1号进程领养。
该父进程结束了就便进入僵尸,则该父进程的父进程是bash,bash除了fork()创建新进程还能帮助回收。
linux中所有的进程都有爹,除了1号进程
- cpu资源分配的先后顺序,就是指进程的优先权(priority)。
- 优先权高的进程有优先执行权利。配置进程优先权对多任务环境的linux很有用,可以改善系统性能。
- 还可以把进程运行到指定的CPU上,这样一来,把不重要的进程安排到某个CPU,可以大大改善系统整 体性能
首先优先级和权限有什么关系呢?
是什么?
优先级:使用“事物”的先后顺序
权限:能不能使用某种“事务”
为什么?
优先级什么场景下有价值?被管理对象多的时候
比如虽然资源种类很多但是大家都迫切需要同一个资源的时候同样也有优先级的问题。
怎么办?
进程优先级?决定了哪个进程优先使用某种资源(常见的如CPU,外设)
struct task_strcut{
//优先级,通过整数来表示,一般数值越小,优先级越高
}
起到的作用:
ps -al #查看当前用户启动的进程,不会把系统内的全显示
ps -aux #系统内全部用户的进程
我们很容易注意到其中的几个重要信息,有下:
- UID:代表执行者的身份
- PID:代表整个进程的代号
- PPID:代表这个进程是由哪个进程发展衍生过来的,即父进程的代号。
- PRI:代表这个进程可被执行的优先级,其值越小越早被执行
- NI:代表这个进程的NICE值
- PRI也还是比较好理解的,即进程的优先级,或者通俗点说就是程序被CPU执行的先后顺序,此值越小的进程的优先级别越高
- 那NI呢?就是我们所要说的nice值了,其表示进程可被执行的优先级的修正数值
- PRI值越小越快被执行,那么加入nice值后,将会使得PRI变为:PRI(new)=PRI(old)+nice
- 这样,当nice值为负值的时候,那么该程序将会优先级值将变小,即其优先级会变高,则其越快被执行
- 所以,调整进程优先级,在Linux下,就是调整进程nice值
- nice其取值范围是-20至19,一共40个级别。
需要强调一点的是,进程的nice值不是进程的优先级,他们不是一个概念。
但是进程nice值会影响到进程的优先级变化。
可以理解nice值是进程优先级的修正数据。
默认情况下,所有进程的优先级(PRI)都是80,NICE都是0。
所以nice[-20,19],pri(new)[60,99],一共是40个级别。
具体看操作系统,centos一般是把pri的初始值80作为PRI(old)
除非特殊情况,否则尽量不要改进程优先级!
- top
- 进入top后按"r"->输入进程pid->输入nice值
centos一般是把pri的初始值80作为PRI(old)
- 竞争性: 系统进程数目众多,而CPU资源只有少量,甚至1个,所以进程之间是具有竞争属性的。为了高效完成任务,更合理竞争相关资源,便具有了优先级
- 独立性: 多进程运行,需要独享各种资源,多进程运行期间互不干扰
- 并行: 多个进程在多个CPU下分别,同时进行运行,这称之为并行
- 并发: 多个进程在一个CPU下采用进程切换的方式,在一段时间之内,让多个进程都得以推进,称之为并发
优先级就很好地表现了进程之间地竞争性。
举例:之前王婆与王婆婚介所的例子。
王婆为了品牌不会被影响,有事给你尽量把事情办了。
王婆->实习生
bash(父进程)->子进程(执行代码,崩溃),不会影响父进程。
基于时间片轮转的多个进程看起来在同时推进的状态,就叫做并发。
比如电脑同时开着qq,微信,钉钉进程。假设给每个进程5ms,那么qq进程时间到了就换到微信,再到了就换到钉钉,如此循环。由于时间太短我们人眼感觉不到,看起来就是同时推进。
多个进程同时在任何一个时刻,同时有多个进程在执行代码,就叫做并行。
调度器:雨露均沾,公平。调度器隶属于操作系统。
时间片是浮动的。
进程在时间片内,不可被抢占。【不可抢占式调度】
进程在时间片内,可以被抢占。【可抢占式调度】
存储器:内存
存储分级
CPU不和外设直接打交道,CPU和内存(为什么?体系结构决定的)
是什么?为什么?怎么办?
输入输出设备:IO(进程,IO是站在谁的角度?进程)scanf,printf,cin,cout
输入:硬盘,键盘,话筒,摄像头,网卡
输出:硬盘,显示器,网卡
CPU:运算,控制
OS:操作系统
作用:做管理工作,管理软硬件。(硬件:冯诺依曼中的所有设备。软件:应用层面:1.安装软件,卸载 2.在系统层面,文件,进程,驱动)【是什么】
如何管理:先描述,再组织!【怎么做】
对进程的管理转化成了对链表的操作。
struct task_struct{
//进程的各种属性
pid,ppid,mm,上下文,pc
}
struct task_struct proc1,proc2,proc3..
proc1->proc2->proc3
OS也是软件,也要先被加载到内存中。
struct task_struct也是一个类型,也要定义变量,也在内存中定义,也占据的是内存空间,OS管理。
在一套系统中,需要有管理者进行统筹。对上,给用户一个稳定的高效的执行环境。对下,管理好软硬件资源,提供稳定的软硬件环境。【为什么】
进程:
是什么:被加载进内存中的程序,运行时的程序,进程=PCB等相关数据结构+程序的代码及数据
为什么:任务需要被计算机完成,任务是可能存在多个很多的,就有了管理的需求,先描述,再组织。PCB等数据结构,任务=代码+数据。
怎么办:进程管理(如何新建,如何调度,如何释放,如何进行其他管理)
PCB{
标识符,pid,ppid
状态:R,S(浅度睡眠),T,D(深度睡眠,IO),x,Z(僵尸状态),孤儿进程
}
ps aux | grep '关键字'
ps aux | grep -v '关键字' #取反
比如我们平时输命令的时候,系统内置的程序命令比如ls,pwd等可以直接用,但是自己写的程序命令就要加上路径。比如./xxx。
执行一条命令:
我们带./
的目的就是为了找到我们的程序。
ls
不用带路径一定有它的默认查找路径。默认查找路径在PATH环境变量中。
环境变量也是变量,那么一定也由变量名(PATH)+变量内容(一串路径)组成
这个$的用法有点像指针,但是两者是不一样的。
当我们输入ls的时候,shell会从这里的路径一个个找,按照:
分割。如果找不到,那么就接着往下面找。找到则执行该路径下的ls
命令。如果都没有,最后就会报command not find
的错误。
同时注意环境变量以:
分割
那么我怎么把自己的命令不带路径执行呢?
两种方法:
把当前绝对路径加到环境变量里(一次性用品,加载到内存。重新登录就没了)
var =100
echo $var #这样只能让var是命令行的局部变量
pwd #复制当前目录
export PATH #加export后是全局变量
export PATH=$PATH:/home/ycb/linux-project/FORK #环境变量以:分割,加上去
把命令复制到已经添加好的环境变量目录中(不推荐:会污染linux自带的命令池)
可以想到,which就是获取环境变量,在环境变量中不断拼接路径看是否存在该路径。
我们平时安装软件的时候,其实是干了什么?
PATH是环境变量中的一个,系统有多个环境变量,用来解决不同的场景
env #查看当前用户下的环境变量
- PATH: 指定命令的搜索路径
- HOME:指定用户的主工作目录(即用户登陆到Linux系统中时,默认的目录)
- SHELL:当前Shell,它的值通常是/bin/bash。
- echo:显示某个环境变量值
- export:设置一个新的环境变量
- env:显示所有的环境变量
- unset:清除环境变量
- set:显示本地定义的shell变量和环境变量
env | cat -n
export val=200
env | grep val
unset val #清除val这个环境变量
我们可以发现定义的val在环境变量里
环境变量是什么?系统中的某些具有一定全局性质的变量,通常为了满足某些系统需求。
为什么要存在环境变量?系统的全局变量都是为了方便用户,开发者,系统进行某种最简单化的查找,定位,确认等等问题。
怎么办?
命令行:env用来查看,export导出环境变量,PATH,SHELL,HOME…
C如何获取环境变量呢?
int main(int argc,char* argc[],char* env[]);//命令行参数个数,命令行参数,环境变量
这里的三个参数是由谁传递给main
函数并且调用的呢?
无论是命令行参数,还是环境变量,目前理解为系统来调用给main
的
ls -a
ls -l
ls -i
ls -u #这就是命令行参数,带选项呈现出不同的参数
#ls是可执行程序,剩下的就是命令行参数
#include
#include
int main(int argc,char* argv[],char* envp[]){
printf("Command-line arguments:\n");
for(int i=0; argv[i]!=NULL; i++){
printf("argv[%2d]:%s\n",i,argv[i]);
}
printf("Enviroment-line arguments:\n");
for(int i=0; envp[i]!=NULL;i++){
printf("envp[%2d]:%s\n",i,envp[i]);
}
printf("\n");
}
#include
#include
int main(int argc,char* argv[],char* envp[]){
if(argc>=2&&strcmp(argv[1],"-a")==0){
printf("执行选项a\n");
printf("hello\n");
}
else if(argc>=3&&strcmp(argv[2],"-g")==0){
printf("执行选项b\n");
printf("world\n");
}
else if(argc==1){//执行默认选项
printf("hello world\n");
}
}
注意C、C++中argv[0]==可执行程序。其他脚本语言可能是参数。具体情况实验一下就知道了。
可以发现代码中打出来的环境变量列表与直接env
的输出是基本完全一致的。
#include
#include
int main(int argc,char* argv[],char* envp[]){
printf("Enviroment-line arguments:\n");
for(int i=0; envp[i]!=NULL;i++){
printf("envp[%2d]:%s\n",i,envp[i]);
}
printf("\n");
}
#include
int main(int argc, char *argv[])
{
extern char **environ;
for(int i=0; environ[i]!=NULL; i++){
printf("%s\n", environ[i]);
}
return 0;
}
libc中定义的全局变量environ指向环境变量表,environ没有包含在任何头文件中,所以在使用时 要用extern声明。
因此比如说要我们写个pwd程序,其实只要获取环境变量中的PWD然后输出即可。
putenv
getenv
#include
#include
int main()
{
printf("%s\n", getenv("PATH"));
return 0;
}
常用getenv和putenv函数来访问特定的环境变量。
#include
#include
using namespace std;
int main( ){//不用写参数也可以,因为不止一种方式传递
cout<<getenv("PWD")<<endl;
}
环境变量具有全局属性,体现在:父进程的环境变量信息,是可以被子进程继承下去。
由于树形结构可以还有子进程。也就是说子进程再新建的子进程也能看到。
所以说父进程的环境变量所有人都能看到,所以说具有全局属性。
#include
#include
using namespace std;
int main( ){
cout<<getenv("MYPWD")<<endl;
}
可以看到此时没有反应因为环境变量中并没有MYPWD。
此时我们在bash(父进程)中设置环境变量。那么由bash创建的子进程t.out
也会有该环境变量了
如果直接只进行 MYPWD1=“helloworld” ,不调用export导出,再用程序查看,会发现env中不存在,自然子进程也看不见。
MYPWD1="helloworld"
背景:
- 32位平台
- kernel 2.6.32
进程地址空间图
共享区里放的一般是动态库和共享内存。
图中看来正文代码是从0x00000000地址开始的,但是实际上不是的。
#include
#include
int init_val=100;
int uninit_val;
int main(int argc,char* argv[],char* env[]){
printf("正文代码:%p\n",main);
printf("初始化数据:%p\n",&init_val);
printf("未初始化数据:%p\n",&uninit_val);
int* tmp=(int*)malloc(sizeof(int)*10);
printf("堆数据:%p\n",tmp);
printf("栈数据:%p\n",&tmp);
printf("命令行参数:%p\n",argv[0]);
printf("命令行参数:%p\n",argv[argc-1]);
printf("环境变量:%p\n",env[0]);
}
进程地址空间是啥意思呢?
举个例子,老虎会有一个自己的领地,在领地中它划分领地有各种区,每个区做什么最好。老虎不可能时时刻刻用着领地的任何部分。
地址空间类似于领地,是对区域的划分。并不是任何时候都被使用,仅仅是限定和衡量了一段内存空间,限定了进程的运行空间。地址空间划分为内核空间,命令行参数环境变量,栈,共享区,堆,初始化数据和未初始化数据,正文代码。
#include
#include
int val=100;
int main(){
int _pid=fork();
if(_pid==0){
//child
val=1000;
while(1){
printf("child: val=%d address-val=%p\n",val,&val);
sleep(1);
}
}
else if(_pid>0){
//parent
sleep(3);
while(1){
printf("parent: val=%d address-val=%p\n",val,&val);
sleep(1);
}
}
return 0;
}
我们一个现象,val变量的地址是一样的,但是val值却是不一样的。从这个现象,可以得出什么结论。
请问进程地址空间是内存吗?
内存中的同一个地址的值,有没有可能被不同的进程读取, 表现出不同的值??
结论:
由于子进程继承了父进程的地址空间,所以两者的进程地址空间是一样的。
在子进程没有对数据进行写的时候,OS为了节省内存,并没有把继承的数据新开辟空间。(写时拷贝)
当子进程进行写的时候,操作系统将虚拟地址映射到了新的物理内存上。所以会有相同虚拟地址空间而实际的值却不同的情况。
每个进程都有一个进程的地址空间。
系统中可能存在多个进程,所以系统中一定存在多个地址空间。
所以,地址空间要不要被管理起来呢?一定要管理,如何管理?
地址空间按照地址空间图那么划分,如何描述呢?
小时候有个同桌之间的概念——三八线。其实和这里就很像。三八线就是在桌子上划分区域,不让越界。这里也是类似的。
那么划分区域就设定起始位置和终止位置。
struct area{
int start;
int end;
}
struct area _A={1,50};
pos<_A.start||pos>_A.end;//判定A越界了
struct area _B={51,100};
struct mm_struct{
unsigned long code_start;
unsigned long code_end;
unsigned long init_data_start;
unsigned long init_data_end;
unsigned long uninit_data_end;
unsigned long heap_end;
unsigned long heap_end;
};
struct mm_struct mm={0x011010,0x20000,.....};
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HHMn3uwu-1638085342375)(https://gitee.com/yyyqwqcode/tupian/raw/master/img/image-20211128105336412.png)]
本质就是一个对应的结构体,描述进程占有资源的相关的一张表。结构体包含的就是区域信息,能够实现区域划分。
**申请空间的本质:**向内存索要空间,得到物理地址,然后在特定区域申请没有被使用的虚拟地址,建立映射关系,返回虚拟地址即可。
为了解决直接访问物理内存导致的访问别人空间和空间不连续的问题,计算机设计者
从用户直接访问内存转变为操作系统访问。地址空间,通过虚拟内存,将空间连续化处理了。
当有了页表之后,OS就可以处理访问异常的情况了。
这里主要有两个情况,一个是越界访问别人的地址空间的情况,由于虚拟地址中没有对应的项所以会直接被中止。
另一种就是越界访问自己的地址空间,由于权限管理不允许就崩溃了。
所以我们有时候代码出问题没有什么影响,有时候直接崩溃。
有时越界不过分,比如栈空间数组访问越界,映射到的物理空间仍然可读可写,程序没啥异常,但是实际逻辑错误。
比如定义指针和字符串常量,指针本来应该访问栈上的空间但是越界到了字符串常量,发现权限不对就直接崩了。
因此存在的意义:
举个有烟火味的例子,没人管理的话去银行存钱直接自己去找对应的保险柜存钱。可能存在拿了别人的钱或者自己的钱太多了溢出到别人的保险柜里了。
现在就是有了银行工作人员,把存钱的位置交给了工作人员,人员通过对应的卡号和密码去找对应保险柜取钱。起到了保存仓库的作用。
我们存钱可以10,20,50,100,一张张存进去,到了某一天取一个整数的金额出来。起到了连续化的作用。
地址空间上呈现的是虚拟地址,访问的时候通过页表映射转换到物理内存拿到对应的代码和数据的。
举个具体的例子:老师手里有学生名单,教室里学生都是乱座的,甚至还有学生是在家网课。老师有名单就不考虑学生在哪,有名单能点到就行。
这里的老师就是进程,名单就是地址空间,教室就是物理内存。
什么叫做进程?
进程是加载进内存的程序,由进程常见的数据结构(struct task_struct(控制块) && struct mm_struct(地址空间),两者指针链接)和代码数据构成。
运行队列,等待队列又怎么理解?
task_struct中是包含了很多的进程链接信息的,运行队列,等待队列本质是把进程PCB进行排队的过程。
task_struct中的程序计数器及内存指针
程序计数器就是指向当前指令的下一条指令,进行代码的运行。
内存指针可以用来找进程地址空间的。
task_struct中的上下文数据?
可以先理解为把数据保存在task_struct
里,但实际上还有全局、局部段描述符
。
task_struct
是内核给申请的,比如说task_struct proc控制块
,这个控制块本身是在内存中开辟的空间,由内核控制的,保存的上下文数据来自寄存器的数据。将寄存器的数据保存到进程中。既然是操作系统要管,那么这个空间是在地址空间的内核空间部分
。