目录
冯诺依曼体系结构
生活中常见的计算机,如台式机,笔记本,以及服务器,大多都遵循冯诺依曼体系结构。
结构图:
输入设备:包括键盘, 鼠标,扫描仪, 写板等。
中央处理器(cpu):含有运算器和控制器。
输出设备:显示器,打印机等。
注意:
- 这里的储存器指的是内存!
- 不考虑缓存情况,这里的CPU能且只能对内存进行读写,不能直接访问输入和输出设备。
- 输入设备和输出设备也只能和存储器即内存打交道。
- 有些设备既可以是输入设备也可以是输出设备,如磁盘,从c++文件函数我们就可以知道,既可以把内存中的数据输出到磁盘,也可以把磁盘中的数据输入到内存中去。
cpu只能直接和内存打交道原因:
这和木桶效应有点相似,cpu的运行速度是最快的,内存其次,输入和输出设备最慢
它们和木桶一样,如果两个交互时候,不是由最快的运行速度决定运行速度,而是由最慢的运行速度决定运行速度,和木桶由最低的挡板决定装水多少一样。所以为了提高效率,cpu只能和内存打交道。
对冯诺依曼的理解,不能停留在概念上,要深入到对软件数据流理解上,请解释,从你登录上qq开始和某位朋友聊天开始,数据的流动过程。从你打开窗口,开始给他发消息,到他的到消息之后的数据流动过程。如果是在qq上发送文件呢?
操作系统
概念
任何计算机系统都包含一个基本的程序集合,称为操作系统(OS)。笼统的理解,操作系统包括:
内核(进程管理,内存管理,文件管理,驱动管理)和其他程序(例如函数库,shell程序等等)
操作系统作用:
- 与硬件交互,管理所有的软硬件资源
- 为用户程序(应用程序)提供一个良好的执行环境
管理的本质是对数据进行处理
那么操作系统怎么管理软硬件呢?
管理由分为管理者,执行者,被管理者,管理者不会直接和被管理者接触,管理者做出决策,执行者去管理被管理者,然后把得到的数据给管理者,管理者通过被管理者的数据,来进行管理。比如操作系统管理硬件,操作系统是管理者,硬件是被管理者,其实他们中间还有一层执行者驱动,操作系统通过驱动来管理被管理者。
计算机组成结构
为了保护操作系统和方便用户使用操作系统,操作系统不允许用户直接接触它管理的软硬件,并对位提供系统调用接口,用户只需要告诉操作系统要进行什么操作,操作系统对用户发出的指令进行操作,只把结果返回给用户,不会让用户看到具体实现,这样就既保护了自己,也满足了用户的需求,但通常系统调用接口是用户很难看懂的,需要用户对操作系统有所了解,在这种背景下,由产生了shell外壳和库函数等等,用户只需要在他们上面操作就行,他们会把用户的需求转化为调用系统接口,然后把结果反馈给用户。
总结:
- 在开发角度,操作系统对外会表现为一个整体,但是会暴露自己的部分接口,供上层开发使用,这部分
由操作系统提供的接口,叫做系统调用。 - 系统调用在使用上,功能比较基础,对用户的要求相对也比较高,所以,有心的开发者可以对部分系统
调用进行适度封装,从而形成库,有了库,就很有利于更上层用户或者开发者进行二次开发。
进程
概念:一个运行起来的程序就是进程,也就是在内存中的程序。
描述进程-PCB(process control block):进程信息被放在一个进程管理块的数据结构里,可以理解为进程属性的集合。
Linux系统下的PCB是叫做tack_struct的结构体结构。
task_struct是Linux内核的一种数据结构,它会被装载到RAM(内存)里并且包含着进程的信息
当多个程序加载进内存中时,操作系统会把PCB加载进内存,如struct tack_struct,每个PCB都存储每个程序的属性,其中包括程序的代码,并把这些PCB通过数据结构比如链表连接起来,然后操作系统直接对数据结构进行管理,通过管理他们来管理程序。
操作系统管理进程总结:
- 先把进程描述起来,即用PCB(struct task_struct)存储进程属性。
- 把进程组织起来,即用数据结构把多个进程的PCB连接起来,直接通过管理数据结构来管理进程。
那么进程是什么样呢?
查看当前进程
其中
ps ajx //查看所有进程
ps ajx | grep mypro //可以通过管道查看指定进程
ps ajx | head -1//其中所有进程第一行是进程属性
与进程相关的系统调用
getpid() :查看进程id
#include <iostream>
#include <unistd.h>
using namespace std;
int main(){
while(1){
cout << "这是一个进程" << "进程id:" << getpid() << endl;
sleep(1);
}
return 0;
}
getppid() : 查看父进程id
可以发现每次程序的进程id都不同,这是因为每次进程都需要在内存中重新分配空间
但是父进程id一样,这是因为它们的父进程是xshell命令行,除非重新连接服务器,不然xshell命令行的进程id不会变,而且xshell命令行的名称是bash
其中父进程不受子进程影响:当你写的程序运行失败,但是你的xshell命令行不受影响,还可以继续使用。
kill -9 [进程id] :杀死进程
fork() : 创建子进程
在调用fork函数前,只有一个父进程,调用fork函数后,有父进程和子进程两个进程,
其中fork()函数给父进程返回子进程的id,子进程返回0。
可以看到两个进程在同时进行
查看进程补充:
其实linux下有一个进程目录 /proc
当进程运行时,会出现在该目录下
当我们把进程执行的程序删除时,进程不会挂掉,还会继续运行
但是该进程的exe指向的程序目录会显示被删除,
没有删除程序之前:
删除程序之后:
操作系统进程状态
每个cpu都有一个运行队列,外设也有一个等待队列,把不同进程的pcb排在它们里面,队列状态表示进程使用cpu和外设的顺序。
所谓的进程状态,就是在不同的等待队列中。
运行状态:在运行队列的pcb对应进程都是运行状态,不管有没有正在使用cpu资源。
阻塞状态:等待状态通常是指进程在等待某些外部事件的发生,例如等待输入/输出操作完成,这个时候操作> 系统会把它移出cpu运行队列,进入其他等待队列。
挂起状态:当进程处于阻塞状态时,内存空间不足,由于进程正在阻塞,需要等待很长时间,为了提高工作>效率,操作系统会将该进程的程序暂时放回磁盘,以便为了其他程序加载进内存。
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 */
}; 比
R运行状态(running): 并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列里
S睡眠状态(sleeping): 意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡眠(interruptible sleep))。
D磁盘休眠状态(Disk sleep)有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个状态的进程通常会等待IO的结束。
T停止状态(stopped): 可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行。
X死亡状态(dead):这个状态只是一个返回状态,你不会在任务列表里看到这个状态
R (run)运行状态:,不一定指程序正在运行中,他指程序正在运行中或处于运行队列中
我们运行以下程序
#include <stdio.h>
int main()
{
while(1)
{
int a = 10;
}
return 0;
}
S(interruptable sleep) 休眠状态: 也叫可中断休眠状态,意味着等待某件事完成,比如io输入输出,它不会占用cpu资源。
执行以下代码:
#include <stdio.h>
#include <unistd.h>
int main()
{
int cnt = 0;
while(1)
{
printf("我正在打印:%d", cnt++);
}
return 0;
}
可以看到,虽然我们看到一直在打印,但是我们查询到的是休眠状态
原因:printf函数是将数据打印到显示器上是外设,所以速度很慢,要等显示器就绪,需要等待很长时间,所以它的状态是睡眠状态(阻塞状态的一种)。一般的运行,99%的时间都是等io就绪,1%的时间是在执行打印代码。
处于S状态的进程,该进程不会占用cpu资源,但是可能会占用内存资源,即阻塞状态;也有可能不占用内存资源,即挂起状态,在Linux系统下不会显示是否处于挂起状态
D(disk sleep) 不可中断休眠状态:正在等待事件发生,并且不能被中断,这些进程通常在等待硬件事件,比如磁盘的输入输出
如果想要停止D状态程序,只能让他自行停止或者断电。
如果有大量的进程处于D状态,可能是系统崩溃的情况,比如大量的磁盘输入输出,这个时候磁盘可能会爆满。
T(stop)停止状态:可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可
以通过发送 SIGCONT 信号让进程继续运行。但是继续运行后进程由原来的前台进程变成后台进程。
处于T状态的进程,该进程不会占用cpu资源,但是可能会占用内存资源,即阻塞状态;也有可能不占用内存资源,即挂起状态,在Linux系统下不会显示是否处于挂起状态
通过kill -19停止程序
此时,状态就变成了T
了解前台进程和后台进程
前台进程:
运行以下程序:
#include <stdio.h>
#include <unistd.h>
int main()
{
int cnt = 0;
while(1)
{
printf("我正在打印:%d", cnt++);
sleep(1);
}
return 0;
}
此时进程处于休眠状态
可以看到s后面有个+号表示前台进程
当我们在打印的地方输入一些指令,比如pwd,ll等待,系统不会响应这些命令,但是按ctrl c会停止运行,这种情况就表面是前台进程。
后台进程
我们先让一个程序暂停,然后再让其继续执行,那么这个进程就会变成后台进程。
后台进程S状态的后面没有+号
此时我们打印界面执行一些命令可以执行
但是当我们用ctrl c停止进程时,没有用
此时只能通过kill -9才能终止进程
t(tracing stop)跟踪停止状态:
在 Linux 中跟踪停止是一项功能,允许您停止进程运行,然后稍后恢复它。这对于调试或解决进程问题非常有用。该信号将使进程处于停止状态。该进程不会使用任何 CPU 资源,但仍会使用一些内存资源。您可以通过向其发送信号来恢复该过程。
比如用gdb调试进程时,进程就处于t状态,
Z(zombie)僵尸状态
僵死状态(Zombies)是一个比较特殊的状态。当进程退出并且父进程(使用wait()系统调用,后面讲)
没有读取到子进程退出的返回代码时就会产生僵死(尸)进程
僵死进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码。
所以,只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程进入Z状态
代码演示
1 #include <stdio.h>
2 #include <unistd.h>
3 #include <sys/types.h>
4 int main()
5 {
6 int cnt = 0;
7 pid_t id = fork();
8 if(id == 0)
9 {
10 for(int i = 0; i < 10; i++)
11 {
12 printf("这是子进程id: %d\n", getpid());
13 sleep(1);
14 }
15 }
16 else{
17 while(1)
18 {
19 printf("这是父进程id: %d\n", getpid());
20 sleep(1);
21 }
22 }
23 return 0;
24 }
可以看到当子进程退出的时候,父进程没有读取子进程结束的代码,子进程就会变成僵尸状态。
X(dead) :死亡状态
当进程退出结果被父进程或操作系统接受之后,就会进入死亡状态,这个状态很快,无法在Linux操作系统中查看。
孤儿进程
在一个linux操作系统中,进程有父子关系,当一个进程的父进程结束了,但是这个进程还没有结束,那么它就会被操作系统领养,操作系统就是它的父进程,这个进程就是孤儿进程。
代码展示
1 #include <stdio.h>
2 #include <unistd.h>
3 #include <sys/types.h>
4 int main()
5 {
6 int cnt = 0;
7 pid_t id = fork();
8 if(id == 0)
9 {
10 while(1)
11 {
12 printf("这是子进程pid: %d, ppid: %d\n", getpid(), getppid());
13 sleep(1);
14 }
15
16 }
17 else
18 {
19 for(int i = 0; i < 5; i++)
20 {
21 printf("这是父进程pid: %d, ppid: %d\n", getpid(), getppid());
22 sleep(1);
23 }
24 }
25 return 0;
26 }
可以看到,当父进程结束后,进程的父进程就是1号进程了,1号进程就是操作系统,而且此时子进程又前台进程变成了后台进程。
进程优先级
cpu资源是有限的,可能多个进程在同一个cpu上运行。
cpu资源分配的先后顺序,就是指进程的优先权(priority)。
优先权高的进程有优先执行权利。配置进程优先权对多任务环境的linux很有用,可以改善系统性能。
//查看进程优先级
ps -l
UID : 代表执行者的身份
PID : 代表这个进程的代号
PPID :代表这个进程是由哪个进程发展衍生而来的,亦即父进程的代号
PRI :代表这个进程可被执行的优先级,其值越小越早被执行
NI :代表这个进程的nice值
PRI and NI
进程当前的优先级为PRI为原来的PRI+NI值
其中NI值是 -20到19,一共20个值,为了避免用户乱改程序优先级,Linux系统的原来的优先级一直都是80,即使用户修改NI值,优先级也只能在60到89之间。
更改优先级命令
用top命令更改已存在进程的nice:
top
进入top后按“r”–>输入进程PID–>输入nice值
其他概念
竞争性: 系统进程数目众多,而CPU资源只有少量,甚至1个,所以进程之间是具有竞争属性的。为了高
效完成任务,更合理竞争相关资源,便具有了优先级
独立性: 多进程运行,需要独享各种资源,多进程运行期间互不干扰
并行: 多个进程在多个CPU下分别,同时进行运行,这称之为并行
并发: 多个进程在一个CPU下采用进程切换的方式,在一段时间之内,让多个进程都得以推进,称之为并发。
进程切换
一个cpu只能被一个进程占用,那么为什么我们平时可以打开很多软件呢,其实每个进程只能占用cpu一定的时间,当这段时间到了之后,不管这个进程有没有完成,它都会取消占用cpu资源,另外一个进程就会占用cpu资源,然后不断循环,由于cpu效率非常高, 所以不同进程来回切换占用cpu资源,这就是进程切换。
环境变量
基本概念和操作
环境变量(environment variables)一般是指在操作系统中用来指定操作系统运行环境的一些参数
如:我们在编写C/C++代码的时候,在链接的时候,从来不知道我们的所链接的动态静态库在哪里,但
是照样可以链接成功,生成可执行程序,原因就是有相关环境变量帮助编译器进行查找。
环境变量通常具有某些特殊用途,还有在系统当中通常具有全局特性
我们平常写代码生成的可执行程序,他是一个可执行程序,可以查看一些命令也都是可执行程序
那么他们都是可执行程序,为啥我们运行代码生成的可执行程序时候,需要带路径,而执行系统命令的时候不需要呢?
但是当我们把test.out拷贝到和pwd一样的系统路径时候,我们也可以不用带路径直接执行了
这是为什么呢?
其实操作系统中存在一些环境变量,比如PATH,在我们执行命令或程序时,他会帮我们在系统中自动检索路径
其中:是路径分隔符,所以执行PATH里面的程序或命令时不需要带路径
那么如何将我们的路径添加进去呢?
可以通过export命令添加
PATH又是从哪来的?
当我们使用命令行解释器bash时候,他会通过一定方式加载PATH,加载方式会在家目录的.bashrc和.bash_profile里
常见的环境变量
PATH : 指定命令的搜索路径
HOME : 指定用户的主工作目录(即用户登陆到Linux系统中时,默认的目录)
SHELL : 当前Shell,它的值通常是/bin/bash。
HISTSIZE :可以保存历史命令的数量
HOSTNAME :主机名
LOGNAME:登入用户名
PWD :查看当前所处位置,pwd底层就是用的它
查看方式
1.查看指定环境变量
echo $[环境变量]
2.查看所有环境变量
env
3.通过代码获取
- getenv()函数
- environ指针
- 命令行参数
getenv使用场景
代码:
1 #include <iostream>
2 #include <cstdlib>
3 #include <cstring>
4 using namespace std;
5 int main()
6 {
7 char* str = getenv("USER");
8 if(strcmp("root", str) == 0)
9 {
10 ;
11 }
12 else
13 {
14 cout << "permission deny" << endl;
15 }
16 return 0;
17 }
~
此时使用非root用户登录运行程序时,
其实,权限的底层就是类似这样实现的。
和环境变量相关的命令:
- echo: 显示某个环境变量值
- export: 设置一个新的环境变量
- env: 显示所有环境变量
- unset: 清除环境变量
- set: 显示本地定义的shell变量和环境变量
其实命令行解释器也可以直接定义本地变量,但本地变量不是环境变量,想要把它变成环境变量 export即可
清除环境变量:
export也可以直接定义环境变量
环境变量具有全局性
环境变量通常是具有全局属性的
环境变量通常具有全局属性,可以被子进程继承下去
#include <stdio.h>
#include <stdlib.h>
int main()
{
char * env = getenv("MYENV");
if(env){
printf("%s\n", env);
}
return 0;
}
直接查看,发现没有结果,说明该环境变量根本不存在
导出环境变量
export MYENV="hello world"
再次运行程序,发现结果有了!说明:环境变量是可以被子进程继承下去的!想想为什么?
实验
如果只进行 MYENV=“helloworld” ,不调用export导出,在用我们的程序查看,会有什么结果?为什
么?
环境变量的组织方式
每个程序都会收到一张环境表,环境表是一个字符指针数组,每个指针指向一个以’\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;
}
- 第三方变量
#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;
}
命令行参数
补充:
获取文件属性系统调用
//看见系统调用接口
man 2 stat
可以获取文件的各种属性,比如文件拥有者权限等等,再配合环境变量,就可以判断当前用户有没有权限了。
进程地址空间
地址空间的基本单位是字节
32位系统下,有2^32个地址
2^32 * 字节 = 4G
每个字节都要有唯一的地址
unsigned int 刚好有这么多
进程地址空间的本质:是一种内核数据结构
进程空间数据结构mm_struct
heap和stack所谓的区域调整,本质就是改变区域的start和end
我们平常看见的地址其实都是虚拟地址,真实地址其实是通过页表映射的地址
每个进程都有自己独立的进程地址空间,进程在物理内存中的位置和虚拟地址空间通过页表映射建立联系
其实当程序未加载进内存时程序只中已经存在地址了。
解释:生成可执行程序需要进程编译和链接操作,链接需要链接库函数,没有地址怎么找到库函数呢,所以未加载进内存之前就已经有地址了,当程序加载进内存时,天然具有了物理地址,而虚拟地址空间直接使用程序中的地址,然后通过页表建立和物理地址空间建立联系。
所以为什么存在进程地址空间呢?
- 如果让进程直接访问物理空间,万一访问非法空间了呢,这样非常不安全。
- 进程地址空间的存在,可以让我们更方便的进行进程和进程数据代码的解耦,保证了进程的独立性。
- 让进程以统一的视角来看待进程对于的代码和数据各个区域,方便使用编译器也以统一的视角来进行编译代码,编译器编译完成后进程可以直接使用。