一.冯诺依曼体系结构
冯诺依曼体系结构是当代计算机的基本结构,它主要包括几个板块,输入设备,输出设备,存储器,运算器和控制器。
下面是简略版的图解析:
输入设备主要包含鼠标,键盘,话筒等主要进行数据的传输;存储器即内存用于将接受输入设备的数据,为数据进行排队后交给CPU处理,在CPU数据返回后再传给输出设备,因此内存是冯诺依曼体系结构最重要的部分;接着是CPU,运算器和控制器结合构成了CPU,运算器主要负责进行代码的逻辑运算和算数运算,控制器主要负责对代码进行控制;最后是输出设备,主要是显示器,喇叭等负责数据的输出。
举个例子,当我们使用微信发信息给朋友时,首先我们将文字通过键盘读取到存储器中,存储器将数据交给CPU进行运算处理,随后返回数据给存储器,最后将数据传给输出设备网卡,你的网卡将数据传到他的网卡中,他的网卡作为输入设备将数据传给他的存储器进行处理后返回数据到你的屏幕上,完成了信息传递。
综上所述,冯诺依曼体系结构中,CPU在数据层面,不会和外设打交道,只会和内存打交道;其中最重要的结构就是存储器结构,存储器处理数据的快慢决定了计算机运行的效率高低。
二.操作系统的理解
有了上述冯诺依曼体系结构的理解,我们可以得知冯诺依曼体系结构就是一堆管理数据处理数据的硬件,硬件在裸机操作上存在复杂性与局限性,因此有了操作系统方便与更好的操作这套体系结构。用通俗的比喻来说,冯诺依曼是一个人的肉体,那么操作系统就是这个人的灵魂。
操作系统(operator system)即OS,分为外壳和内核。内核主要是控制硬件(冯诺依曼体系结构),而外壳主要是用于与用户进行交互。所以说OS搭建起了我们人与底层硬件之间的桥梁,起到了承上启下的作用。对上,为用户程序很好的与用户进行交互;对下,与硬件交互,管理所有的软硬件资源。
其中用户部分就属于外壳,系统软件部分为Linux的内核部分。
下面我们就来详细介绍Linux的内核部分。
Linux内核是一个管理底层硬件的管理者,类似于公司的老板,老板通常只参与一些决策性的工作,给谁加薪裁哪个员工等。那么老板是通过什么来得知该做什么决策呢?通过秘书对每一位员工的表现进行数据处理得知。那么,我们可以把每一个硬件抽象为每一个员工,老板则是Linux内核操作系统,操作系统通过对硬件的各种描述对其作出相应决策。而描述的内容自然需要用数据结构进行存储。所以说,在操作系统上,每一个硬件都可以抽象描述成一个个struct结构体,由老板操作系统进行组织。
三.进程
3.1 进程基本概念与操作
进程就是程序执行的一个过程,程序执行需要软硬件的资源信息,这些资源信息被储存在PCB(Process Control Block)进程控制块中。在Linux下,进程控制块就是:struct task_struct 的一个结构体。task_struct 对包含进程的信息对进程进行了描述,通过对task_struct的增删查改来对进程进行组织操作。
那么task_struct里应当包含哪些内容呢?
标识符用来记录当前进程的pid,父进程ppid,以及用户uid信息(后续详细讲解)。进程的状态,例如运行,阻塞,终止等。优先级就是每个进程都有自己的优先级系数,优先级高的会先执行。程序计数器就是当该进程暂时挂起时,会先将代码存储到寄存器中,当再次运行到该程序时,通过地址就能再次找回。内存管理信息,里面储存代码段,数据段,堆栈上的信息………….
那么该如何查看进程呢?
通过指令 ls /proc 可以查看系统文件的所有进程。这里我们写了一个文件用于输出当前进程的pid并且无限循环。
如图当前进程的pid为109988
我们可以通过 ls /proc 在系统中查询到。当进程结束时,就无法查询到当前进程 。
我们可以通过 ps aux | grep + 文件的方式查看特定文件的进程。
当进程运行时从PCB上获取该进程的pid。
3.2 进程状态
什么是进程状态?进程状态有什么用?
进程状态通俗讲类似于我们工作上班,有工作,休息,有事外出等不同的状态。进程也是同理,会有运行,就绪,挂起,终止等状态。在PCB中,我们用不同的数字来表示不同的状态,反之内存通过不同的数字就能识别出当前进程的各种状态作出不同的运算处理。
下面放上一张进程状态图:
进程在初始阶段分配资源创建变量时为创建状态,进程创建好了,CPU运行当前进程属于运行状态;而其他进程排队等待前面的进程运行完毕叫做就绪状态;当我们需要对键盘输入数据时,进程进入阻塞状态,当输入完成后又重新进入就绪状态;运行完成后结束。
进程运行时task_struct进入队列中,在排队的过程处于就绪状态;当需要进程进入阻塞状态时,代码和数据仍会在内存中保留,若此时内存空间不足时,会将task_struct中的代码和数据迁移到磁盘中,进入挂起状态。直到完成输入后才会重新进入就绪状态。
下面是Linux源码中的不同状态
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 */
};
running 对应就绪状态运行状态;sleeping 为睡眠状态,对应于阻塞状态,例如需要数据输入等,此时进程是可以被唤醒的;disk sleep 深度睡眠状态,此时进程无法被唤醒,数据存储在磁盘中;stopped 状态,此时进程停止运行,进程的上下文会被保存起来,需要外部信号重新开始;tracing stop 当内存资源不足时,会强行终止进程运行。zombie状态,为僵尸状态,当子进程结束后,父进程没有对子进程状态进行读取,子进程进入僵尸进程,类似于调用了资源确没有回收产生内存泄漏。
与僵尸进程类似的孤儿进程,孤儿进程就是父进程先退出的情况,此时子进程无法被回收,就会由1号进程进行领养回收。
3.3 进程优先级
CPU资源分配的先后顺序,就是进程的优先权。优先权高的可以先执行,我们可以通过 ps -l 的指令来查看系统进程,其中PRI为优先级,NI为nice值,nice值和PRI的和就是最终的优先级大小。这个数越小,程序执行的越早。
nice值的取值范围在-19 到 20之间,一共40个档位。
进程运行有并发和并行两种模式,并发就是同时发生,通过CPU不停的切换进程使进程同时运行;并行就是共同运行,有两个或以上的CPU同时处理。
在并发情况下,每个进程都有自己对应的时间片,时间片消耗完就会切换进程,而当前进程的上下文数据就会暂时保存(拷贝)在CPU的寄存器当中,当下一次运行到该进程时再拿出来。
3.4 进程调度
进程调度由进程的优先级和状态决定,在Linux中进程调度有进程调度队列 run_queue 管理信息。队列里包含各个进程task_struct的地址指针,用于识别该进程的优先级以及状态。
run_queue中的三个参数 nr_active ,bitmap【5】,queue【140】。nr_active是指针,queue队列用来存储不同优先级的进程指针地址,数字下标就对应了它的优先级系数;而bitmap位图就是记录不同的优先级下是否存在有进程。这样就对进程进行了排队操作,此时我们再来一套这样的结构,将运行完的进程放入第二个结构中进行排队操作,当第一个结构中的进程全部运行完成后,将两者的nr_active指针内容互换,这样就完成了排队。
四. 环境变量
环境变量是操作系统特定用途的变量,这些信息可以被操作系统、应用程序或用户进程访问和使用,以影响软件的运行行为、路径查找。常见的环境变量有 PATH 系统运行时查找可执行文件的路径;HOME 当前用户的主目录,常用于默认路径;USER 当前操作人;PWD 当前目录。
我们可以通过 echo + $变量名 来查询到环境变量在哪个目录下。
env 可以查询到当前系统中所有的环境变量。
set 和 unset 是用于设置和清除环境变量的操作 ,unset 删除后的环境变量重启后仍会存在。
环境变量是用于描述系统环境的,而环境表是将环境组织起来的数据结构。环境表中包含了各个环境变量的键值,通过键值查找就能找到该环境变量。环境表会从父进程上继承到子进程上。
五. 程序地址空间(虚拟地址空间)
我们先来看一下程序地址空间是如何分布的。
我们通过代码来查看一下地址信息。
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int g_unval;
int g_val = 100;
int main(int argc, char* argv[], char* env[])
{
const char* str = "helloworld";
printf("code addr: %p\\n", main);
printf("init global addr: %p\\n", &g_val);
printf("uninit global addr: %p\\n", &g_unval);
static int test = 10;
char* heap_mem = (char*)malloc(10);
char* heap_mem1 = (char*)malloc(10);
char* heap_mem2 = (char*)malloc(10);
char* heap_mem3 = (char*)malloc(10);
printf("heap addr: %p\\n", heap_mem); //heap_mem(0), &heap_mem(1)
printf("heap addr: %p\\n", heap_mem1); //heap_mem(0), &heap_mem(1)
printf("heap addr: %p\\n", heap_mem2); //heap_mem(0), &heap_mem(1)
printf("heap addr: %p\\n", heap_mem3); //heap_mem(0), &heap_mem(1)
printf("test static addr: %p\\n", &test); //heap_mem(0), &heap_mem(1)
printf("stack addr: %p\\n", &heap_mem); //heap_mem(0), &heap_mem(1)
printf("stack addr: %p\\n", &heap_mem1); //heap_mem(0), &heap_mem(1)
printf("stack addr: %p\\n", &heap_mem2); //heap_mem(0), &heap_mem(1)
printf("stack addr: %p\\n", &heap_mem3); //heap_mem(0), &heap_mem(1)
printf("read only string addr: %p\\n", str);
for (int i = 0; i < argc; i++)
{
printf("argv[%d]: %p\\n", i, argv[i]);
}
for (int i = 0; env[i]; i++)
{
printf("env[%d]: %p\\n", i, env[i]);
}
return 0;
}
下面是运行结果:
我们可以通过运行代码结果来反推程序地址空间的正确性。
我们可以观察出变量地址 已初始化变量 < 未初始化变量 < 堆 < 栈 < 环境变量 ,其中在堆与栈之间地址高度相差较大,这段空间就是我们的共享区。因此我们的堆和栈是相对而生的,堆从低地址向高地址累加,栈从高地址向低地址挂起。
下面我们再来看一组代码
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int g_val = 0;
int main()
{
pid_t id = fork();
if(id < 0){
perror("fork");
return 0;
}
else if(id == 0){ //child,⼦进程肯定先跑完,也就是⼦进程先修改,完成之后,⽗进程再
读取
g_val=100;
printf("child[%d]: %d : %p\\n", getpid(), g_val, &g_val);
}else{ //parent
sleep(3);
printf("parent[%d]: %d : %p\\n", getpid(), g_val, &g_val);
}
sleep(1);
return 0;
}
我们发现父子进程输出的地址是一致的,但是输出的变量确不一样,这是为什么呢?
说明输出的变量并不是同一个变量,它们在物理空间上的地址不一样。因此,并不存在所谓的程序地址空间,在Linux下,被称为虚拟地址空间。
在虚拟地址空间上,每个进程都被许诺有可以使用内存大小的空间,但实际上并不会用到这么多的空间。不同进程之间即使虚拟地址名一致,但它们在底层的物理地址并不是同一块,这样即使进程不同,也不会影响各个进程使用自己的地址空间。在task_struct中存在一个成员变量 mm_struct 的地址指针,mm_struct 中有类似start end两个指针来为虚拟地址划分实际的物理空间。再通过一张页表将实际地址与虚拟地址做键值对应,这才是操作系统实际的内存分配。
希望各位大佬多多支持!!!
评论前必须登录!
注册