我们熟悉的计算机——笔记本,不熟悉的计算机——服务器,他们都是在冯诺依曼体系结构的基础上,底层搭载不同的硬件结构,上层由操作系统管理的。冯诺依曼体系结构如下图所示:
- 输入设备:包括键盘、鼠标、磁盘、网卡、摄像头、话筒等
- 存储器:内存
- 中央处理器:包含运算器和控制器
- 输出设备:显示器、磁盘、网卡、音响等
在这里我们需要注意以下几点:
- 存储器所指的是内存,并非磁盘,硬盘等存储器件。
- CPU只能与内存进行读写,不能访问输入输出设备
- 输入输出设备读写数据也只能与内存进行交互
总结:内存是体系结构的核心设备,CPU与外设之间的信息交互都需要依靠内存!!
什么是操作系统呢?
操作系统就是一款专门针对软硬件资源进行管理工作的软件
为什么需要操作系统呢?
对下:与硬件交互,管理所有的软硬件资源
对上:为用户程序(应用程序)提供一个良好的执行环境
操作系统如何管理?
先描述:用struct结构体描述对象
再组织:用链表或者其他搞笑的数据结构进行组织
系统调用是操作系统对外提供的一些接口,供上层开发使用。
库函数是存放在函数库中的函数。
那么系统调用和库函数有什么关系呢?
系统调用和库函数是上下层关系,库函数是用户对系统调用的进一步封装,库函数对硬件进行操作时,会调用系统提供的API。
通俗来讲,进程就是一个正在执行的程序。在这里我们理解为进程由进程控制块PCB、数据和代码构成。当然进程不止这几部分,还有进程地址空间和页表等。
操作系统中同时存在许多进程,每个进程各不相同,操作系统如何管理不同的进程呢?
用操作系统的六字真言“先描述,再组织”,描述进程用到进程控制块PCB,在Linux操作系统下的PCB称为:task_struct。
task_struct中存储的进程的信息,其主要可以分为以下几类:
- 标识符:也叫做PID,描述本进程的唯一标识符,用来区别其他进程
- 状态:任务状态、退出代码、退出信号等
- 优先级:相对于其他进程的优先级
- 程序计数器:PC指针,用于保存程序下一条执行指令的地址
- 内存指针:包括程序代码和进程相关数据的指针,通过内存指针可以找到程序文件
- 上下文数据:进制执行时CPU的寄存器中数据
- I/O状态信息:包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表等
- 记账信息:包括处理器时间总和,使用的时钟数总和、时间限制、记账号等
- 其他信息
上下文数据非常重要,因为在进程切换时,寄存器中的数据会被保存在PCB中,为了下次切换回来时,CPU可以找到上次进程运行的地方。通过上下文数据,我们才可以感受到进程是被切换的。
查看进程的方法有两种:
首先我们写一个死循环的程序,让程序一直运行着,并输出该进程的PID和PPID。
#include
#include
#include
using namespace std;
int main()
{
while(1)
{
std::cout << "pid =" << getpid() << " ppid =" << getppid() << std::endl;
sleep(1);
}
return 0;
}
- 通过/proc系统文件查看
- 通过ps - axj | grep 文件名
我们通过系统调用fork函数创建进程
#include
#include
#include
int main()
{
// fork的验证
int ret = fork();
std::cout << "ret =" << ret << ",proc =" << getpid() << " parent =" << getppid() << std::endl;
sleep(1);
return 0;
}
运行该程序,我们可以得到以下结果:
从以上结果我们可以得到以下结论:
- 创建子进程后,会有两个返回值,父进程的返回值是子进程的PID,子进程的返回值为0
- 父进程和子进程的代码共享
但通常情况下,fork创建子进程后,我们需要通过返回值进行分流操作,目的是为了子进程与父进程做不一样的事情。
#include
#include
#include
int main()
{
cout << "I am parent: pid =" << getpid() << ",ppid =" << getppid() << endl;
pid_t ret = fork();
if(ret == 0)
{
while(1)
{
cout << "I am child: pid =" << getpid() << ",ppid =" << getppid() << ",ret =" << ret << endl;
sleep(1);
}
}
else if(ret > 0)
{
while(1)
{
cout << "I am parent: pid =" << getpid() << ",ppid =" << getppid() << ",ret =" << ret << endl;
sleep(2);
}
}
else
{
cout << "fork failed" << endl;
}
sleep(1);
return 0;
}
- 由于PC指针的存在,fork创建的子进程并不执行fork语句前的代码
通过以上两段程序,我们该如何理解fork创建进程呢?
fork创建进程表示系统中多了一个进程,而进程由与进程相关的内核数据结构和进程的数据和代码组成。
那么子进程的内核数据结构、数据和代码从何而来呢?
子进程的task_struct:会以父进程为模板,初始化子进程的task_struct
子进程的代码:和父进程共享一份代码,因为程序运行的时间,代码无法被修改
子进程的程序:默认情况下,子进程和父进程数据共享,但是当数据发现改变的时间,会“写时拷贝”
进程控制块中有一个叫进程状态的信息,进程状态标志着此进程当前的运行情况。一个进程可以有以下几种状态:
SIGSTOP
信号停止进程。发送 SIGCONT
信号让进程继续运行。#include
#include
#include
int main()
{
pid_t ret = fork();
if(ret > 0)
{
while(1)
{
cout << "I am parent: pid =" << getpid() << ",ppid =" << getppid() << ",ret =" << ret << endl;
sleep(2);
}
}
else if(ret == 0)
{
while(1)
{
cout << "I am child: pid =" << getpid() << ",ppid =" << getppid() << ",ret =" << ret << endl;
sleep(20);
exit(1);
}
}
else
{
exit(1);
}
sleep(1);
return 0;
}
运行结果如图所示:
僵尸进程如果一直不进行处理,PCB需要一直维护,占用系统的空间;同时一个父进程创建了许多子进程,如果不回收就会造成内存资源的泄漏。
孤儿进程
#include
#include
#include
int main()
{
pid_t ret = fork();
if(ret > 0)
{
cout << "I am parent: pid =" << getpid() << ",ppid =" << getppid() << ",ret =" << ret << endl;
sleep(10);
exit(1);
}
else if(ret == 0)
{
while(1)
{
cout << "I am child: pid =" << getpid() << ",ppid =" << getppid() << ",ret =" << ret << endl;
sleep(1);
}
}
else
{
exit(1);
}
sleep(1);
return 0;
}
进程优先级表示CPU资源分为的先后顺序,也就是指进程的优先权。
如何查看进程的优先级呢?
- ps -l
进程的PRI默认值都是80,用户通过调整NICE值修改进程的优先级,而NICE的取值范围为(-20~19)
NI的取值范围较小原因:优先级再怎么设置,也只能是一个相对的优先级,不能出现绝对的优先级,否则会出现“饥饿问题”
top指令修改已存在进程的NICE值。
top -> 按r -> 输入进程的PID -> 输入nice值
环境变量一般是指操作系统中用来指定操作系统运行环境的一些参数。
环境变量通常具有某些特殊用途,在系统中通常具有全局特性
常见的环境变量:
- PATH:指定命令的搜索路径
- HOME:指定用户的主工作目录
- SHELL:当前Shell,通常是/bin/bash
和环境变量相关的命令
- echo:显示某个环境变量值
- export:设置一个新的环境变量
- env:显示所有环境变量
- unset:清除环境表里
- set:显示本地定义的Shell变量和环境变量
查看环境变量的方法
echo $Name
int main(int argc, char* argv[], char *env[])
{
for(int i =0; env[i]; ++i)
{
printf("%d -> %s\n", i, env[i]);
}
return 0;
}
#include
int main(int argc, char *argv[])
{
extern char** environ;
int i = 0;
for(; environ[i]; i++){
printf("%s\n", environ[i]);
}
return 0;
}
#include
#include
int main()
{
printf("%s\n", getenv("PATH"));
printf("%s\n", getenv("HOME"));
printf("%s\n", getenv("SHELL"));
return 0;
}
环境变量通常是具有全局属性的
我们通过设定了一个本地变量MYENV = “sherry”
#include
#include
int main()
{
char * env = getenv("MYENV");
if(env)
{
printf("%s\n", env);
}
return 0;
}
首先在命令行设定一个本地变量MYENV
,通过set | grep MYENV
显示出本地变量的值,但运行env | grep MYENV
和./myproc
发现无法在环境变量中找到MYENV
,说明此时MYENV只是一个本地变量,不是环境变量,通过export
命令,将其设置为环境变量,再运行env | grep MYENV
和./myproc
可以发现已经有输出结果。
当MYENV
变为环境变量后,不仅仅可以通过命令输出,还可以通过程序输出,因此可以证明环境变量具有全局属性。
在学习C语言的时候,我们经常看见下面这张图:
通过一段代码打印出不同区域的地址可以帮我们理解区域的划分:
#include
#include
#include
#include
int g_unval;
int g_val = 100;
int main()
{
const char* s = "sherry";
printf("code addr:%p\n", main);
printf("string rdonly addr:%p\n", s);
printf("int addr:%p\n", &g_val);
printf("unint adde:%p\n", &g_unval);
char* heap = (char*)malloc(10);
printf("heap addr:%p\n", heap);
printf("stack addr:%p\n", &s);
int a = 1;
int b = 1;
int c = 1;
printf("stack addr:%p\n", &a);
printf("stack addr:%p\n", &b);
printf("stack addr:%p\n", &c);
}
那么这里的内存地址是不是我们经常说的物理内存呢?
我们通过以下代码进行验证:
#include
#include
#include
#include
int main()
{
int g_val = 100;
int cnt = 5;
int ret = fork();
while(cnt)
{
if(ret > 0)
{
printf("parent[%d]: %d: %p\n",getpid(), g_val, &g_val);
}
if(ret == 0)
{
if(cnt == 3)
{
g_val = 200;
}
printf("child[%d]: %d: %p\n",getpid(), g_val, &g_val);
}
cnt--;
sleep(1);
}
return 0;
}
通过运行结果可以发现,在子进程中修改了g_val的值,父进程的g_val不改变,因为“写时拷贝”的原因,但是我们惊奇的发现父进程和子进程的g_val的地址一模一样。
结论:这里的地址绝对不是物理地址,而我们把他叫做虚拟地址
进程的虚拟地址和系统物理地址又有什么关联呢?
进程的虚拟地址本质上是内核上的一中数据类型,可以用结构体来描述。
struct mm_struct
{
int code_strat;
int code_end;
int init_data_strat;
int init_data_end;
int uninit_datae_strat;
int uninit_datae_end;
int heap_strat;
int heap_end;
int head_strat;
int head_end;
}
虽然每个进程都一个自己的mm_struct,但是每个进程都认为他的mm_struct代表整个内存,且所有的地址为0x0000…000~0xFFFF…FFF。
虚拟地址通过*_start和*_end的形式把自己划分成不同的区域,不同的区域有着自己的地址界限。
通过页表和MMU建立虚拟地址和物理地址的映射关系。
OS为何要通过页表来实现虚拟地址和物理地址的映射关系呢?
- 通过添加一层软件层,完成对进程的内存操作进行管理,本质是为了保护物理内存及各个进程的安全
用户的误操作可能会越界访问不属于自己的地址空间,对其他地址上的内容进行修改等。使用虚拟内存,就有效保护了真实物理内存空间上的内容。
- 将内存申请与内存使用在时间上划分清楚,通过虚拟地址空间来屏蔽底层申请内存的过程,达到进程读写内存和OS进行内存管理操作,进行软件上的分离
也许进程会开辟一块很大的空间,但是进程并不是立即使用,因此OS可以开辟虚拟空间,物理内存用于一些真正需要的地方,等到进程需要使用时,再为其开辟空间,这样有效的提高效率。
- 站在CPU的角度,进程统一看作使用4GB,而每个空间区域的相对位置是比较确定的
操作系统只为了达到一个目的:每一个进程都认为自己是独占系统资源的!
通过以上的进程地址空间学习后,以后我们再描述进程,就不仅仅是PCB、数据和代码了。
进程 = 进程控制块task_struct + 进程地址空间mm_struct + 页表 + 数据和代码