Linux学习(系统,进程,
我们常见的计算机,如笔记本。我们不常见的计算机,如服务器,大部分都遵守冯诺依曼体系
截至目前,我们所认识的计算机,都是有一个个的硬件组件组成
输入单元:包括键盘, 鼠标,扫描仪, 写板等
中央处理器(CPU):含有运算器和控制器等
输出单元:显示器,打印机等
这里的存储器指的是内存,也就是买电脑时的比如8G/16G等,内存的数据掉电易失;磁盘等都是外存具有永久存储能力。
运算器+控制器+其他=CPU;
内存和外存直接的数据交互成为IO过程。

冯诺依曼,必须强调几点:
这里的存储器指的是内存 不考虑缓存情况,这里的CPU能且只能对内存进行读写,不能访问外设(输入或输出设备) 外设(输入或输出设备)要输入或者输出数据,也只能写入内存或者从内存中读取。 一句话,所有设备都只能直接和内存打交道。因为外存如磁盘的速度很慢,CPU运算很快,内存较快,所以数据都要加载到内存中,或者是运算完写出到内存再存储到外存。(体系结构规定)
对冯诺依曼的理解,不能停留在概念上,要深入到对软件数据流理解上,请解释,从你登录上qq开始和某位朋友聊 天开始,数据的流动过程。从你打开窗口,开始给他发消息,到他的到消息之后的数据流动过程。大概如下图:从键盘输入你好加载到内存经过CPU加密然后加载到内存在到输出显示器/网卡,经过网络来到朋友的电脑网卡接收,加载到内存,CPU解密到内存再到显示器显示。

操作系统(operator system)
操作系统是一个进行软硬件资源管理的软件
管理软硬件的本质就是对数据进行管理;
如:在学校校长是管理者,学生是被管理者,校长和我们不怎么见面是如何管理学生呢?实质就是校长拿到了学生的数据(如通过辅导员拿到学生数据),并且对学生的数据进行管理进而就是对学生进行了管理。管理者和被管理者不需要直接交互,依然能够把被管理者管理起来。在这里校长相当于OS,辅导员相当驱动,学生相当于是硬件。

一个学校里面学生很多,校长拿到学生的数据很多,为了更好的管理这些数据,校长想到一个办法就是“先描述,在组织”;将学生的数据用struct描述起来,然后使用链表或其他高效的数据结构进行管理。所有的管理方法都是:先描述,在组织。
对软件的管理呢?
假设我们去银行办事,会有大堂经理指引填表等操作,然后到对应的窗口办理业务。银行是给我们提供服务的为什么我们只能在这些窗口办理我们的业务,不能进入后台自己办理?银行不可能让我们进入后台自己办理业务。因为银行提供服务的同时要保护自己的安全。
同样的操作系统要给我们提供服务也要保护自己的安全,那么操作系统就提供了类似银行窗口的接口给我们使用,但是这些接口对使用者的能力要求比较高,所以有大佬将这些接口进一步封装,方便人们使用,如shell外壳等。

什么是进程?
一个运行起来(加载到内存)的程序就是进程。加载到内存的进程需要被管理。怎么管理?同样是先描述,再组织。将进程的信息存放到一个叫进程控制块(PCB,process control block)的数据结构中,在Linux操作系统下的PCB是:task_struct的结构体。如:

所谓的对进程管理,转化为了对进程对应的PCB进行管理或者上图的链表进行增删查。
进程=内核数据结构(task_struct)+进程对应的磁盘代码
查看进程
通过指令:ll /proc

创建一个子进程,通过fork()函数;有两个返回值,成功创建则返回子进程pid给父进程,返回0给子进程。创建失败返回-1。getpid()获得子进程的pid;getppid()获得父进程的pid。(死循环按ctrl+c结束)

1 #include <stdio.h>
2 #include <unistd.h>
3
4 int main()
5 {
6 int ret = fork();
7 if(ret < 0)
8 {
9 perror("fork");
10 return 1;
11 }
12 else if(ret == 0)
13 { //child
14 while(1)
15 {
16 printf("我是子进程 : 我的pid:%d!, 父进程pid: %d\n", getpid(),getppid());
17 }
18 sleep(1);
19 }
20 else
21 { //father
22 while(1)
23 {
24 printf("我是父进程 : 我的pid:%d!, 父进程pid: %d\n", getpid(),getppid());
25 sleep(2);
26 }
27 }
28
29 sleep(1);
30 return 0;
31 }

查看进程的状态:ps axj;ps aux(这里搭配了之前说的head和grep和管道的使用)。pid是子进程的,ppid是父进程的

进程的状态
进程状态:运行,新建,就绪,挂起,阻塞,等待,停止,挂机,死亡……
目前我只知道运行,挂起,阻塞
下图描述了运行,阻塞状态。

挂起状态就是,在假如内存中加载了很多进程,但是内存空间不足了,某一个进程一直在等待外设的资源,比如上图磁盘,需要等很久,此时操作系统将该进程的代码和数据暂时保存在磁盘上,节省一部分空间给其他进程,此时这个进程的状态就是挂起。将进程的相关数据,加载或保持到磁盘的操作称为:内存数据的唤入唤出。
一个进程可以有几个状态(在Linux内核里,进程有时候也叫做任务)下面的状态在kernel源代码里定义:
/*
* 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):这个状态只是一个返回状态,你不会在任务列表里看到这个状态。

grep指令的进程是R运行状态,刚刚创建的子进程父进程是S状态,因为使用了sleep同时也有printf等操作,S状态也是阻塞的一种,暂停状态也是阻塞的一种。D状态,一般是该进程状态当内存空间占满时OS可能会杀掉(结束)某些进程给其他进程挪空间时候,D状态的进程不能被杀掉,相当于免死金牌,只能通过掉电或者进程自己醒来。(带+号的是前台进程可以使用ctrl+c结束,不带的是后台进程不能使用ctrl+c结束,用kill -9 pid数值;结束)
演示T状态:

在双开的另外一个指令页面输入:kill -19 pid数值,就是暂停该进程,kill -18 pid数值,就是恢复该进程。

僵尸状态Z(zombie)
子进程退出,父进程还在运行,但父进程没有读取子进程状态(没有回收),子进程进入Z状态(目前我只知道这么多)
一个终端运行代码,另一个终端下启动监控和终止子进程;

僵尸进程危害
进程的退出状态必须被维持下去,因为他要告诉关心它的进程(父进程),你交给我的任务,我办的怎么样了。可父进程如果一直不读取,那子进程就一直处于Z状态?是的! 维护退出状态本身就是要用数据维护,也属于进程基本信息,所以保存在task_struct(PCB)中,换句话 说,Z状态一直不退出,PCB一直都要维护?是的! 那一个父进程创建了很多子进程,就是不回收,是不是就会造成内存资源的浪费?是的!因为数据结构 对象本身就要占用内存,想想C中定义一个结构体变量(对象),是要在内存的某个位置进行开辟空间!
孤儿进程
父进程先退出,子进程就称之为“孤儿进程”
孤儿进程被1号进程领养,当然要有1号进程回收。
一个终端运行代码,另一个终端下启动监控和终止父进程;


进程优先级
CPU资源分配的先后顺序,就是指进程的优先权(priority)。 优先权高的进程有优先执行权利。配置进程优先权对多任务环境的linux很有用,可以改善系统性能。 还可以把进程运行到指定的CPU上,这样一来,把不重要的进程安排到某个CPU,可以大大改善系统整体性能。
ps -l;指令查看进程信息
UID : 代表执行者的身份;PRI :代表这个进程可被执行的优先级,其值越小越早被执行 ;NI :代表这个进程的nice值;优先级计算:PRI(新) =PRI(初始)+NI;PRI(初始)固定是80;NI的范围:【-20,19】;也就是说优先级调整范围为【60,89】。

用top命令更改已存在进程的nice: 进入top后按“r”->输入进程PID->输入nice值
进程地址空间
以32位为例
创建一个test.c;makefile文件
test.c:
#include<stdio.h>
2 #include<unistd.h>
3
4 int global_val =100;
5 int main()
6 {
7 pid_t id=fork();// 创建子进程
8 if(id<0)
9 {
10 printf("fork error\n");
11 return 1;
12 }
13 else if(id==0)
14 {
15 int cnt=0;
16 while(1)// 循环打印子进程的pid和它父进程的pid
17 {
18 printf("我是子进程,pid:%d, ppid:%d | global_val:%d, &global_val:%p\n ",getpid(),getppid(),global_val,&global_val);
19 sleep(1);
20 cnt++;
21 if(cnt==10)
22 {
23 global_val=300;
24 printf("子进程已经更改全局变量了.......\n");
25 }
26 }
27 }
28 else
29 {
30 while(1)
31 {
32 printf("我是父进程,pid:%d, ppid:%d | global_val:%d, &global_val:%p\n ",getpid(),getppid(),global_val,&global_val);
33 sleep(2);
34 }
35 }
36 sleep(1);
37 return 0;
38 }
makefile:
1 mytest:test.c
2 gcc -o mytest test.c
3
4 .PHONY:clean
5
6 clean:
7 rm -f mytest
make之后:

从上图可以看见子进程修改了全局变量的值,但是父进程依然打印的是原来的值。奇怪的是他们打印的全局变量的地址居然是一样的!!!这是为什么呢?
其实打印的地址都是是虚拟地址,而不是物理地址。
学C/C++的时候会了解到数据存放在哪些区域,比如动态申请的内存空间是在堆区,函数调用和局部变量是在栈区,等等。其实这些地址空间都是虚拟地址。

前面我们说到,进程就是内核数据结构(struct_task_struct)+进程对应的磁盘代码。
每一个进程在运行队列中时,它们都是认为自己占据了整个内存空间的内存,其实并不是,内存空间的内存是所有进程共享的。 每一个进程都有自己的进程地址空间,这些进程地址空间也是通过先描述后管理的模式运行。这些区域都是通过mm_struct结构体管理的。然后给每一个区域的start和end设定值(画38线)
mm_struct结构体,比如:
struct mm_struct
{
unsigned int code_start,code_end;
unsigned int heap_start,heap_end;
unsigned int stack_start,stack_end;
//……
};
描述地址空间:1个字节=8个比特位,32位机下,2^32*1字节=4GB地址空间范围;区域的划分其实就是调整start和end(画38线)。内核代码mm_struct截图


创建一个进程后,OS为这个进程创建一个内核数据结构(struct_task_struct,里面会有一个struct mm_struct *mm用来管理mm_struct)来管理该进程的状态等,同时给它申请空间(mm_struct)(该进程以为的4GB空间),mm_struct里面就包含了各个区域的start和end的记录。其中栈区和堆区的调整实际就是修改各个区域的start和end的值,如malloc和new或者函数调用,局部变量创建就是栈和堆的增大;free和delete,函数调用结束,局部变量销毁就是栈和堆的缩小。

比如我们的执行程序test.exe,运行时操作系统创建进程内核和地址空间,进程地址空间和物理内存空间之间存在一个映射,页表完成映射的功能。比如原本chart c=a;现在c=A;进程根据虚拟地址通过页表映射到物理地址然后将a改为A。(每一个进程都有自己的页表,页表没画的那么简单,是多级页表,目前我还没学)

如下两个进程:

上面为什么子进程修改global_val的值后子进程打印300,父进程打印100,但是打印地址都一样呢?是因为子进程是由父进程创建,所以子进程的内核数据都是拷贝父进程的,global_val是父子进程共享的数据,内核数据结构一样所以&global_val的虚拟地址是一样的。子进程修改共享值的时候,这个过程是操作系统帮我们先开辟空间拷贝100的值,然后让子进程修改,在让子进程的映射地址指向新的物理地址。物理地址变了但是父子进程代码中&global_val取的都是地址空间的虚拟地址还是一样的。只是子进程的页表对应这个地址的映射到物理地址变了所以打印300。父进程的没有改变所以还是打印100。

我们写好的可执行程序代码都是有地址的(可以在vs里面调试,跳转到反汇编就能看到相关地址,main函数也有地址,运行的时候先调用main函数,vs里面有函数先运行然后调用我们的main函数,这里涉及到函数栈帧知识),这些地址的编排规则和进程地址空间编排地址的规则是一样的。


CPU读取的是指令,首先读取到main函数的虚拟地址,在通过页表映射找到main真实地址开始逐句运行main函数的代码,运行到func函数时得到的时func函数的虚拟地址,在通过页表映射到func函数真实地址然后运行他的代码,运行a++时也是通过页表映射在修改a的值。(CPU得到的都是代码的虚拟地址(逻辑地址)这些地址是编译器完成的,编译器和操作系统都是遵守同一套规则,所以编完即可直接使用。
(例子:把学生比喻为代码,辅导员为CPU,学校为内存。同学被录取的时候就有了虚拟地址(学号)当同学们入学了就会有具体的宿舍号--具体物理地址(代码被加载到内存),班长相当于是mm_struct,班长有一个手册记录每一个学号的学生对应的宿舍号(页表);导员要找10号同学,那么班长通过学号10号在页表中映射到具体宿舍号码再找到10号同学。)

为什么存在地址空间:
1、如果让进程之间访问物理内存,万一进程越界非法操作呢(修改别人内存内的数据)? 非常不安全(页表会拦截不安全的访问,只会映射到你的空间);
2、地址空间的存在,可以更方便的进行进程和进程的数据代码的解耦,保证了进程独立性这样的特征;
3、让进程以统一的视角,来看待进程对应的代码和数据等各个区域,方便使用;编译器也已统一的视角来进行编译代码(规则是一样,编完即可直接使用)
更多推荐


所有评论(0)