前言:本期对计算机的软硬件体系结构进行梳理,包括计算机体系结构,什么是操作系统,为什么存在操作系统,操作系统如何进行管理,以及建立在这些软硬件基础上的各种提供给用户进行操作的接口。对于理解操作系统本身以及下一节的进程概念,甚至对整个Linux系统编程的理解都有着至关重要的作用。
CPU只能被动的接受别人传递过来的数据和指令,然后将运算得到的结果返回。
那么这里就会出现两个问题:
1、CPU如何能够识别我们传递给它的数据和指令?
答案是CPU内部有一套自己的指令集,它会把指令对应到指令集,然后完成相应的操作;其中CPU的指令集是二进制的,这就是为什么我们编写的代码需要经过编译链接变成二进制的可执行程序后才能被运行的原因
– CPU需要读懂我们的指令才能完成对应的运算;这也是编译器存在与产生的根本原因。2、CPU需要的数据从哪里获取? 答案是内存
(此处不考虑缓存);虽然我们的数据是存放在磁盘中的,但是由于磁盘读取与写入数据的速度太慢了 –
可以简单理解为CPU的运算速度以纳秒为单位,内存的运算速度以微秒为单位,而磁盘的运算速度则是以毫秒甚至秒为单位;CPU如果直接从磁盘中读取和写入数据,会有大量的等待时间,所以为了提高计算机的整体效率,CPU只会向内存中读取和写入数据;内存再向磁盘中读取和写入数据;其中,内存向磁盘读取和写入数据就是IO的过程。
经过上面的学习,我们可以得到如下结论:
在数据层面上,CPU不会直接和外设打交道,而只会和内存打交道;同样,所有的外设需要载入数据时,只能载入到内存,内存要写入数据,也只能写入到外设中。
有了上面的知识铺垫后,我们就可以解释为什么 程序运行必须加载到内存 中了 – CPU需要从程序中读取数据,但是CPU只和内存打交道,而我们的程序是存储在磁盘中的。
最后,我们对冯诺依曼的理解,不能停留在概念上,要深入到对软件数据流理解上,以下面这个例子为例:我们在QQ聊天窗口向别人发送一条消息到别人接受消息的过程中,数据的流动过程?如果发送的是文件呢?
用户访问软硬件的需求,比如从磁盘中读取与写入数据、向显示器打印数据、通过网卡发送数据等;操作系统会给用户提供系统调用的接口,即当用户有访问软硬件的需求时,直接调用操作系统提供的接口,然后由操作系统来帮助用户完成对应的工作;这样即满足了用户的需求,又保护了软硬件资源。避免恶意操作(如删除磁盘驱动、向磁盘中添加恶意数据等等;)
注:Linux 操作系统是Linus Torvalds于1991年使用C语言编写的,而上述的各种系统调用接口又是由操作系统提供的,所以它们也是C式的接口,说白了就是 用C语言编写的用于用户调用的各种函数接口。
虽然操作系统为我们提供了各种系统调用接口让我们来访问软硬件,但是这些接口在使用上功能比较基础,对用户的要求也相对较高;于是人们在系统调用接口的基础上开发出了用户操作接口,比如 Linux 下的外壳程序 shell,各种函数库 (C/C++等),windows 图形化界面 (GUI),以及一些指令 (编译好的可执行程序) 等;
用户通过这些操作接口进行指令操作、开发操作以及管理操作等等;比如 Linux 下外壳程序 bash 提供的 ls指令,本质上是调用系统接口,将磁盘中文件信息写入到显示器;touch 本质是调用系统接口,在磁盘上创建文件;又比如 C++的 cin/cout 函数,底层都是调用系统调用接口从键盘读入数据/向显示器上打印数据。
进程:一个运行起来(加载到内存)的程序
进程 = 进程对应的磁盘代码 + 进程对应的内核数据结构(PCB)(process control block)
有了进程我们就要进行管理,即 PCB (进程控制块),在Linux下就是 task_struct
task_ struct 内容分类
# Makefile
myproc:myproc.c
gcc -o myproc myproc.c
.PHONY:clean
clean:
rm -f myproc
//myproc.c
#include
#include
int main()
{
while(1)
{
printf("I am a process.\n");
sleep(1);
}
return 0;
}
查看指定进程并显示标题:ps ajx | head -1 && ps ajx | grep 'myproc'
(注意:-1
是提取第一行)
关闭进程:kill -9 + 标识符PID
我们给myproc.c加点东西,演示下系统调用
#include
...
printf("I am a process.My ID is: %d\n",getpid());
...
[hins@VM-12-13-centos test_process]$ ./myproc
I am a process.My ID is: 10772,PPID is: 30650
I am a process.My ID is: 10772,PPID is: 30650
^C
[hins@VM-12-13-centos test_process]$ ./myproc
I am a process.My ID is: 10785,PPID is: 30650
I am a process.My ID is: 10785,PPID is: 30650
^C
[hins@VM-12-13-centos test_process]$ ./myproc
I am a process.My ID is: 10791,PPID is: 30650
I am a process.My ID is: 10791,PPID is: 30650
^C
[hins@VM-12-13-centos test_process]$ ps ajx | head -1 && ps ajx | grep 30650
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
30650 10806 10806 30650 pts/1 10806 R+ 1001 0:00 ps ajx
30650 10807 10806 30650 pts/1 10806 S+ 1001 0:00 grep --color=auto 30650
26536 30650 30650 30650 pts/1 10806 Ss 1001 0:00 -bash
发现PID一直在变,而PPID就不变,当退出服务器再重新登录后,PPID才会变
命令行上启动的进程,一般它的父进程没有特殊情况的话,都是bash!
程序运行都是子进程负责,原因是一旦程序出错,父进程可以及时收到错误信息并反馈给用户,而不是直接挂掉。
int main()
{
// 创建子进程 -- fork是一个函数。 函数执行前:只有一个父进程;函数执行后:父进程+子进程
pid_t id = fork();
printf("I am a process.My ID is: %d,PPID is: %d, id:%d\n",getpid(),getppid(),id);
sleep(2);
return 0;
}
[hins@VM-12-13-centos test_process]$ ./myproc
I am a process.My ID is: 15734,PPID is: 30650, id:15735 # 父进程
I am a process.My ID is: 15735,PPID is: 15734, id:0 # 子进程
我们发现,同一个变量id,在后续不会被修改的情况下,竟然有不同的内容。
下面再给代码加个判断观察一下
int main()
{
// 创建子进程 -- fork是一个函数。 函数执行前:只有一个父进程;函数执行后:父进程+子进程
pid_t id = fork();
if(id == 0)
{
//子进程
while(1)
{
printf("子进程, pid: %d, ppid: %d, id: %d\n", getpid(), getppid(), id);
sleep(1);
}
}
else if(id > 0)
{
// parent
while(1)
{
printf("父进程, pid: %d, ppid: %d, id: %d\n", getpid(), getppid(), id);
sleep(2);
}
}
else{ }
return 0;
}
[hins@VM-12-13-centos test_process]$ ./myproc
父进程, pid: 17106, ppid: 30650, id: 17107
子进程, pid: 17107, ppid: 17106, id: 0
子进程, pid: 17107, ppid: 17106, id: 0
父进程, pid: 17106, ppid: 30650, id: 17107
子进程, pid: 17107, ppid: 17106, id: 0
子进程, pid: 17107, ppid: 17106, id: 0
父进程, pid: 17106, ppid: 30650, id: 17107
子进程, pid: 17107, ppid: 17106, id: 0
子进程, pid: 17107, ppid: 17106, id: 0
父进程, pid: 17106, ppid: 30650, id: 17107
......
fork() 之后,会有父进程+子进程两个进程在执行后续代码
fork后续的代码,被父子进程共享!
通过返回值不同,让父子进程执行后续共享的代码的一部分!
这就是多进程。
有了PCB结构体和数据结构,很大程度上能够提升管理进程的效率,但远远不够。我们需要将进程进行分类,我们引入状态这个概念。通过状态。每个进程都有自己的状态,这些状态能够告诉操作系统我正在干什么、我将要干什么,也就是说,进程的多种状态,本质都是为了满足未来的某种使用场景。
状态有:运行、新建、就绪、挂起、阻塞、等待、停止、挂机、死亡
task_struct
结构体对象放入运行队列中!runqueue
,就是R
,不是这个进程正在运行,才是运行状态。那么我们接下来着重介绍的便是三种状态:
实际上,不同的进程状态,其本质就是处于不同的队列。
/*
* 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 */
};
源码关于进程状态的描述是一个指针数组,其中各个字母的含义为:
那么想要在Linux环境下观察进程的状态,我们可用的指令有两个:
ps aux
或者 ps axj
查看系统所有的进程ps -lA
也是能够查看系统的所有进程// 测试代码
while(1)
{
int cnt = 0;
int a = 0;
a = 1 + 1;
printf("当前a的值是: %d, running flag: %d\n", a, cnt++);
}
原因就在于这个printf,它打印到显示器上(外设),而硬件都比较慢,等显示器就绪,要花比较长的时间(CPU)
因此可能有99%的时间在等IO就绪,1%时间在执行代码
[hins@VM-12-13-centos test_process]$ kill -19 9900 #暂停
[hins@VM-12-13-centos test_process]$ ps axj | head -1 && ps axj | grep myproc
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
690 9900 9900 690 pts/0 690 T 1001 0:29 ./myproc
3926 10022 10021 3926 pts/1 10021 S+ 1001 0:00 grep --color=auto myproc
[hins@VM-12-13-centos test_process]$ kill -18 9900 #继续
[hins@VM-12-13-centos test_process]$ ps axj | head -1 && ps axj | grep myproc
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
690 9900 9900 690 pts/0 690 R 1001 0:31 ./myproc
3926 10587 10586 3926 pts/1 10586 R+ 1001 0:00 grep --color=auto myproc
[hins@VM-12-13-centos test_process]$ kill -9 9900 #完全停止
[hins@VM-12-13-centos test_process]$ ps axj | head -1 && ps axj | grep myproc
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
3926 10809 10808 3926 pts/1 10808 S+ 1001 0:00 grep --color=auto myproc
#状态后带+是前台进程,不带+的是后台进程,不能Ctrl + C关掉进程
一个进程被创建出来,完成任务后,要知道它完成的如何,则进程退出时不能立即释放该进程对应的资源!而是保存一段时间后,让父进程或者OS来进行读取,这个状态就是僵尸状态,读取后释放内存,这个进程才是死亡状态
模拟僵尸状态
创建子进程,让父进程不要退出,而且什么都不做,让子进程正常退出。
# Makefile
myprocess:myprocess.c
gcc -o $@ $^ -g
.PHONY:clean
clean:
rm -f myprocess
#include
#include
#include
int main()
{
pid_t id = fork();
if(id == 0)
{
//child
printf("I am child process, pid: %d, ppid: %d\n", getpid(), getppid());
sleep(5);
exit(1);
}
else
{
//parent
while(1)
{
printf("I am parent proceass, pid: %d, ppid: %d\n", getpid(), getppid());
sleep(1);
}
}
return 0;
}
查看进程:ps ajx | head -1 && ps ajx | grep 'myprocess' | grep -v grep
(不显示grep进程)
编写一个每1s就显示进程的脚本:while :; do ps ajx | head -1 && ps ajx | grep 'myprocess' | grep -v grep; sleep 1; done
[hins@VM-12-13-centos test_process_2]$ ./myprocess
I am parent proceass, pid: 15603, ppid: 9091
I am child process, pid: 15604, ppid: 15603
I am parent proceass, pid: 15603, ppid: 9091 # 1s
I am parent proceass, pid: 15603, ppid: 9091 # 2s
I am parent proceass, pid: 15603, ppid: 9091 # 3s
I am parent proceass, pid: 15603, ppid: 9091 # 4s
I am parent proceass, pid: 15603, ppid: 9091 # 5s
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND # 1s
9091 15603 15603 9091 pts/0 15603 S+ 1001 0:00 ./myprocess
15603 15604 15603 9091 pts/0 15603 S+ 1001 0:00 ./myprocess
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND # 2s
9091 15603 15603 9091 pts/0 15603 S+ 1001 0:00 ./myprocess
15603 15604 15603 9091 pts/0 15603 S+ 1001 0:00 ./myprocess
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND # 3s
9091 15603 15603 9091 pts/0 15603 S+ 1001 0:00 ./myprocess
15603 15604 15603 9091 pts/0 15603 S+ 1001 0:00 ./myprocess
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND # 4s
9091 15603 15603 9091 pts/0 15603 S+ 1001 0:00 ./myprocess
15603 15604 15603 9091 pts/0 15603 S+ 1001 0:00 ./myprocess
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND # 5s
9091 15603 15603 9091 pts/0 15603 S+ 1001 0:00 ./myprocess
15603 15604 15603 9091 pts/0 15603 Z+ 1001 0:00 [myprocess]
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND # 6s
9091 15603 15603 9091 pts/0 15603 S+ 1001 0:00 ./myprocess
15603 15604 15603 9091 pts/0 15603 Z+ 1001 0:00 [myprocess]
僵尸进程的危害:
模拟孤儿状态
#include
#include
#include
int main()
{
pid_t id = fork();
if(id == 0)
{
//child
while(1)
{
printf("I am child process, pid: %d, ppid: %d\n", getpid(), getppid());
sleep(1);
}
}
else
{
//parent
while(1)
{
printf("I am parent proceass, pid: %d, ppid: %d\n", getpid(), getppid());
sleep(1);
}
}
return 0;
}
I am parent proceass, pid: 21726, ppid: 9091
I am child process, pid: 21727, ppid: 21726
Killed # 父进程被干掉
[hins@VM-12-13-centos test_process_2]$ I am child process, pid: 21727, ppid: 1
I am child process, pid: 21727, ppid: 1
I am child process, pid: 21727, ppid: 1
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
9091 21726 21726 9091 pts/0 21726 S+ 1001 0:00 ./myprocess
21726 21727 21726 9091 pts/0 21726 S+ 1001 0:00 ./myprocess
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND #父进程被干掉
1 21727 21726 9091 pts/0 9091 S 1001 0:00 ./myprocess
# 1号进程就是操作系统,现在这个子进程被1号进程“领养”了,那么这个进程就是孤儿进程,且会自动变成后台进程
# 如果不领养,对应的僵尸进程,就没有人能回收了。
[hins@VM-12-13-centos test_process_2]$ kill -9 21726 # 杀掉父进程
可以看到干掉父进程后,父进程没有出现僵尸状态。这是为什么呢?
其实父进程也有父进程,即爷爷进程,而bash就是这个爷爷进程,它会帮你把这个父进程回收。
ps -l
F S UID PID PPID C PRI NI ADDR SZ WCHAN TTY TIME CMD
0 S 1001 32079 9091 0 80 0 - 1053 hrtime pts/0 00:00:00 myprocess
0 R 1001 32139 21153 0 80 0 - 38332 - pts/2 00:00:00 ps
最终优先级 = 老的优先级(80) + nice值
PRI and NI
PRI vs NI
用top命令(可能需要sudo)更改已存在进程的nice:进入top后按“r”–>输入进程PID–>输入nice值
F S UID PID PPID C PRI NI ADDR SZ WCHAN TTY TIME CMD
0 S 1001 655 9091 0 60 -20 - 1054 hrtime pts/0 00:00:00 myprocess
0 R 1001 1453 21153 0 80 0 - 38332 - pts/2 00:00:00 ps
cpu中有一个eip寄存器(PC指针),指向下一条指令的地址。
进程切换的理解: 由于Linux的CPU一次只能运行一个进程,但是我们一个时间段内却可以运行多个进程,因为CPU是足够快的,因此我们人感觉的一瞬间就相当于CPU的一个时间段,想一想1ms对于人来说算是一瞬间,但是CPU却是以纳秒为单位计时的,因此在我们自身感觉到的一瞬间也就是CPU的一个时间段内,会将执行的多个进程按照一定的周期分别运行,一个运行到固定周期之后就强行拉入运行队列的末尾等待,就这样直到完成所有执行的进程,这就是进程之间在一定的时间内相互切换,叫做进程切换。而所谓的周期就是时间片。(并发中提到)
进程的上下文保护:
当CPU在进行进程切换的时候,要进行进程的上下文保护,当进程在恢复运行的时候,要进行上下文进程的恢复!
上下文是什么呢?
首先进行感性的理解:当你由于应征入伍离开学校,保留学籍的过程即上下文保护;回到学校重新开始学习生活,恢复学籍的过程即上下文恢复。
进程在运行时会产生非常多的临时数据,同时CPU中存在一套寄存器硬件,当进程运行时,进程的PCB会被放入CPU内的寄存器中,此时CPU就可以通过进程PCB(暂时理解成PCB)得到进程代码数据的地址;CPU在运行进程时所产生的大量的临时数据也都会被保存在寄存器中;因此在进行进程切换时需要进行进程的上下文保护与上下文恢复,进程停止运行时将寄存器里面的数据保存起来,进程重新运行时将保存的数据再放入到寄存器中;所以进程的上下文就是一个进程完成他的时间片后所保存的数据。
注:寄存器硬件 != 寄存器内的数据
进程在运行的时候,占有CPU,进程不是一直要占有到进程结束!如while(1)
CPU寄存器硬件被所有进程共享,但CPU在具体运行某一进程时,CPU寄存器中的数据只属于该进程;同时,我们进行上下文保护时保存的是寄存器中的数据,而不是寄存器硬件。
OK,以上就是本期知识点“进程概念”的知识啦~~ ,感谢友友们的阅读。后续还会继续更新,欢迎持续关注哟~
如果有错误❌,欢迎批评指正呀~让我们一起相互进步
如果觉得收获满满,可以点点赞支持一下哟~