目录
准备阶段
冯 ·诺依曼体系结构
我们所熟知的大部分计算机都遵循冯·诺伊曼体系结构
截至目前,我们认识的计算机,它们都由一个个硬件组成:
- 输入单元:键盘,鼠标,磁盘,网卡等
- 中央处理器(CPU):含有控制器,运算器等
- 输出单元:显示器,音响,磁盘,网卡等
关于冯·诺依曼:
- 这里的存储器指的是内存
- 不考虑缓存情况,CPU只能对内存进行读写,不能访问外设(输入输出设备)
- 外设(输入和输出设备)要输入和输出数据,也只能通过内存
举个例子:登录qq,张三给李四发送消息 “你好”,“你好”这条消息从键盘输入到内存中,然后CPU经过一系列处理将“你好”还给内存,再由内存交给网卡通过网络发送给张三(具体怎么通过网络发送这里省略),张三通过网卡接收到内存中,再由CPU处理后进入内存,最后再由张三的显示器从内存中读取“你好”然后打印
操作系统(Operator System)
操作系统概念
何计算机都有一个基本的程序集合,称为操作系统(OS)。笼统的说,操作系统包括:
- 内核(进程管理,文件管理,内存管理,驱动管理)
- 其他程序(如函数库,shell程序)
操作系统作用
- 与硬件交互,管理好软硬件资源
- 为用户程序提供一个良好的运行环境
操作系统的定位
操作系统就是一款搞 "管理" 的软件
管理是什么?六子真言:先描述,在组织
- 描述:把一件事物描述起来,用struct结构体;
- 组织:再用高效的数据结构组织起来
系统调用和库函数
- 操作系统不相信任何人,但它又需要供用户使用。所以操作系统对外表示为一个整体,提供自己的部分接口,叫做系统调用。
- 系统调用的功能比较基础,对用户的使用要求较高,所以有心的开发者对部分系统调用进行了适度封装,从而形成了库,有了库,对上层应用者和开发者开发提供了便利。
了解了这些知识后,那么一个操作系统是怎么管理一个个进程的呢?很简单,还是六字真言:
先描述,在组织。用一个结构体将进程描述起来,再用高效的数据结构将这些进程组织起来。
1.进程
基本概念:
课本概念:程序的一个执行实例,正在执行的程序
内核概念:担当分配系统资源(cpu时间,内存)的实体
1.1 描述进程-PCB
- 进程信息被放在一个叫做进程控制块的数据结构中,可以理解为进程属性的集合
- 课本上称为PCB(process control block),Linux操作系统下的PCB是:task_struct
1.1.1 task_struct - PCB的一种
- 在Linux下描述进程的结构体叫做task_struct
- task_struct是Linux内核的一种结构体,它会被装载到RAM(内存)里并且包含着进程信息
1.1.2 task_struct内容分类
标识符:描述本进程的唯一标识符,用来区别其他进程
状态:任务状态,退出代码,退出信号等
优先级:相对于其他进程的优先级
程序计数器:程序中即将被执行的下一条指令的地址
内存指针:包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
上下文数据:进程执行时处理器的寄存器中的数据
I/O状态信息:包括显示的IO请求,分配给进程的I/O设备和进程使用的文件列表
记账信息:可能包括处理器时间总和,使用的时钟数总和,时间限制,记帐号等
其他信息
1.2 组织进程
可以在内核源代码里找到它,所以运行在系统里的进程都以task_struct链表的形式存在内核里
1.3 查看进程
进程的信息可以通过/proc系统文件夹查看
要获取进程pid为1的进程信息,需要查看/proc/1/这个文件夹
大多数进程信息还可以通过ps和top这些用户级工具来获取
1.4 通过系统调用获取进程标识符
- 子进程ID:PID
- 父进程ID:PPID
#include<iostream>
#include<unistd.h>
#include<sys/types.h>
using namespace std;
int main()
{
cout << "you can see me" << endl;
while(1)
{
pid_t id = getpid();
pid_t pid = getppid();
cout << "我的pid是:" << id << endl;
cout << "我的ppid是:" << pid << endl;
sleep(1);
}
return 0;
}
1.5 通过系统调用创建进程 - - fork
- 通过查询man手册了解fork - - man fork
- fork有两个返回值
- fork父子进程代码共享,拥有各自独立的数据(写实拷贝技术)
#include<iostream> #include<unistd.h> #include<sys/types.h> using namespace std; int main() { pid_t id =fork(); if(id == 0) { cout << "I am child, my pid :" << getpid() << "my ppid: "<< getppid() << endl; } else if(id > 0) { cout << "I am child, my pid :" << getpid() << "my ppid: "<< getppid() << endl; } else{ return 1; } sleep(1); return 0; }
fork之后通常要用if进行分流:
1.6 进程状态
1.6.1 来看看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):这个状态只是一个返回状态,你不会在任务列表里看到这个状态
1.6.2 进程状态查看
- ps aux / ps axj 命令
1.6.3 Z(zombie) - - 僵尸进程
- 僵死状态(zombies)是一个比较特殊的状态。当子进程退出而父进程没有读取到子进程退出的返回代码就会产生僵死进程。
- 僵死进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码。
- 所以,只有子进程退出,父进程还在运行,但父进程没有读取到子进程状态,子进程进入Z状态
来创建一个僵尸进程的例子:
#include<iostream>
#include<unistd.h>
#include<stdlib.h>
using namespace std;
int main()
{
pid_t id = fork();
if(id == 0)
{
while(1)
{
cout << "i am child : my pid " << getpid() << " my ppid" << getppid() << endl;
sleep(1);
break;
}
exit(0);
}
else
{
while(1)
{
cout << "i am fatherd : my pid " << getpid() << " my ppid" << getppid() << endl;
sleep(1);
}
}
return 0;
}
然后复制ssh渠道,运行监控脚本
while :; do ps axj | head -1 && ps axj | grep zombie_proc |grep -v grep; sleep 1; echo "====================================================="; done
运行进程
![]()
启动监控脚本
可以看到子进程的状态由S变成了Z
1.6.4 僵尸进程的危害
- 进程的退出状态必须被维护下去,因为它需要告诉它的父进程,你交给我的任务,我办的怎么样了。那父进程一直不读取,那子进程就会一直处于Z状态。
- 维护退出状态本身就是要用数据维护,也属于进程基本信息,所以保存在task_struct(PCB)中,换句话说,Z进程一直不退出,PCB一直都要维护。
- 一个父进程创建了很多进程但一直不回收,会造成内存资源的浪费。因为数据结构对象本身就要占用内存。
- 会造成内存泄漏
1.6.5 孤儿进程
当父进程先退出了,而子进程后退出,进入Z状态后该如何处理?
父进程先退出,子进程就叫做"孤儿进程"
孤儿进程会被1号进程领养,也由1号进程回收
1.7 进程优先级
1.7.1 基本概念
- cpu资源的分配资源的先后顺序,就是指进程的优先级(priority)
- 优先权高的进程拥有优先执行权力。配置进程优先权对多任务处理的Linux很有用马,可以改善系统性能
- 还可以把进程运行到指定的cpu上,这样一来,把不重要的进程安排到某个进程,可以大大改善系统整体性能。
在命令行输入:ps -l
- UID:代表执行者的身份
- PID:代表这个进程的代号
- PPID:代表这个进程是由那个进程衍生出来的,即父进程
- PRI:代表这个进程可被执行的优先级,值越小越先被执行
- NI:代表这个进程的nice值
1.8 环境变量
1.8.1 基本概念
- 环境变量(environment variables)一般是指操作系统中用来指定操作系统运行环境的一些参数
- 如:在编写C/C++代码的时候,链接时,从来就不知道我们所链接的动静态库在哪里,但是照样可以链接成功,生成可执行程序,原因就是有相关环境变量帮助编译器进行查找
- 环境变量通常具有某些特殊用途,在系统中通常具有全局属性
1.8.2 常见环境变量
- PATH:指定命令的搜索路径
- HOME:指定用户的主工作目录
- SHELL:当前shell,它的值通常是/bin/bash.
1.8.3 查看环境变量
- echo $NAME //NAME你的环境变量名
1.8.4 环境变量相关命令
- echo:显示某个环境变量值
- export:设置一个新的环境变量
- env:显示所有环境变量
- set:显示本地定义的环境变量和shell变量
- unset:清除环境变量
1.8.5 环境变量组织方式
每个程序都会收到一张环境表,环境表是一个字符指针数组,每个指针指向一个以\0结尾的环境字符串
1.8.6 通过系统调用接口获取环境变量
#include <stdio.h>
#include <stdlib.h>
int main()
{
printf("%s\n", getenv("PATH"));
return 0;
}
常用getenv和putenv函数来获取特定的环境变量
1.8.7 环境变量的全局属性
- 环境变量通常具有全局属性,可以被子进程继承下去
2 程序地址空间
首先看下面这张图
在C/C++时候就已经接触过,知道这是地址空间的布局。但在内存中真的是按照这样的方式严格划分的吗?
我们来实验一下:
写一段代码
#include<iostream> #include<unistd.h> using namespace std; int g_val = 100; int main() { pid_t id= fork(); if(id== 0) { //child cout << "I am child : my g_val = " << g_val << " &g_val = " << &g_val << endl; } else { cout << "I am father : my g_val = " << g_val << " &g_val = " << &g_val << endl; } return 0; }
运行它后我们发现父进程和子进程的g_val不仅值相同,地址也相同,也可以理解,因为子进程就是以父进程为模板创建的,父子并没有对变量做出任何修改。
但将代码稍加改动一下:
#include<iostream> #include<unistd.h> using namespace std; int g_val = 100; int main() { pid_t id= fork(); if(id== 0) { //child g_val = 1000; cout << "I am child : my g_val = " << g_val << " &g_val = " << &g_val << endl; } else { cout << "I am father : my g_val = " << g_val << " &g_val = " << &g_val << endl; } return 0; }
输出结果:
这个时候就奇了怪了,一个变量怎么可能有两个不同的值呢?难道我以前学的C/C++都白学了吗?这个时候就要明确观点了:
- 这个变量内容不一样,所有父子进程输出的一定不是同一个变量
- 虽然地址值相同,但一个变量不可能存放两个值,所以这里出现的地址一定不是物理地址
- 那不是物理地址是啥?Linux下把它叫做虚拟地址
- 所以我们在C/C++当中看到的地址全部都是虚拟地址,物理地址由OS管理,用户看不到!
3.进程地址空间
前面说的"程序的地址空间"是不准确的,当我们写的程序被运行在操作系统后会变成一个个的进程,所以准确的说应该是进程地址空间
前面的部分已经了解到了程序运行被加载到内存中变成一个进程,OS创建PCB - - task_struct来专门维护这个进程,task_struct中存放着进程相关的信息,那么虚拟地址空间是是什么呢?就是下面这张图了
程序在被加载到内存后,OS不仅会为该进程创建一个PCB结构体task_struct维护该进程的信息,还会创建一个虚拟地址空间的结构体mm_struct来划分各个区域,像堆栈等;task_struct存储指向mm_struct的指针mm_struct* mm,这样二者就链接起来了。
那访问虚拟地址最后还不是要访问内存吗,这不是脱裤子放屁吗
举个例子:
当你过年收到了1000块钱压岁钱,想去小卖部买买买,但这个时候你的妈妈说,这个钱我收起来,你需要了我在给你,这样你就不会乱花了。所以当你想要访问压岁钱买教科书买文具你妈肯定大手一挥同意,但你要是说妈我要买变形金刚,你妈肯定之间拒绝你对压岁钱的访问所以加上了虚拟空间,就像是给你和小卖部之间加上了你妈,保护了压岁钱。所以虚拟地址空间的存在还是很有用处的。
那么我有虚拟地址空间我怎么访问物理内存?不可能我说访问就访问吧。
分页&虚拟地址空间
当然不是,OS在虚拟地址空间跟物理地址空间之间创建了一个叫做页表的东西,虚拟地址通过页表(MMU)映射物理内存。
#include<iostream>
#include<unistd.h>
using namespace std;
int g_val = 100;
int main()
{
pid_t id= fork();
if(id== 0)
{
//child
g_val = 1000;
cout << "I am child : my g_val = " << g_val << " &g_val = " << &g_val << endl;
}
else
{
cout << "I am father : my g_val = " << g_val << " &g_val = " << &g_val << endl;
}
return 0;
}
概念说完了,回到之前的代码上,我们知道了这是虚拟地址,但是虚拟地址相同又是怎么回事呢?
因为OS会为每个进程都创建一个各自独立的task_struct和进程地址空间mm_struct,在子进程不进行对变量写时,父子进程看到的g_val的值和地址都相同,两个虚拟地址映射的物理内存也一样,但一旦父子进程对其修改,就会发生写时拷贝,尽管地址还相同,但实际经过页表的重新映射,父子进程实际存放g_val的物理内存已经不同了。 但他们各自独立的地址进程的地址是相同的,只不过经过了页表的映射指向了不同的空间。
画张图梳理一下思路:
当子进程对g_val进行改变时发生写时拷贝,子进程重新映射
一个问题:程序在编译的时候,形成可执行程序之后,程序的内部有没有地址?
答案是已经有地址了,因为虚拟地址不仅仅是操作系统遵守,编译器也要遵守。即代码在编译的时候,编译器已经给我们形成了各个区域,代码区,数据区......,并且采用和Linux内核一样的编址方式,给每一个变量每一行代码都进行了编址。故,程序在编译的时候,每一个字段早已具有了一个虚拟地址。
程序内部的地址,依旧用的是编译器编好的虚拟地址。当程序被加载到内从中,每个变量每行代码都具有了一个物理地址,外部的。
所以CPU在读到指令时,指令内部其实也有地址。