一、冯诺依曼体系结构:
现代计算机的硬件体系结构(规定了现代计算机应该具有那些硬件单元)
包含了五大硬件单元:
- 输入设备:鼠标,键盘,麦克风等
- 输出设备:显示器,打印机等
- 存储器:内存条
- 运算器:中央处理器—cpu
- 控制器:中央处理器—cpu
注意:
- 图中存储器指的是内存
- 不考虑缓存情况,CPU智能对内存就行读写,不能直接访问输入和输出设备
也就是说,所有的计算机硬件全部围绕存储器工作,存储器在中间起到数据缓冲的作用。
使用内存作为存储器的原因:内存的吞吐量大。
存储器分内存和外存,是因为没有一种内存可以既速度飞快,又保存东西长久,还便宜。所以,用内存和外存的组合来解决这个问题。
- 存储器有两个作用,一个是存储文件,关机不丢失,另一个是运行程序,和cpu交换数据。简单讲,二者的区别是,外存是存储东西的,内存是运行程序的。
- 硬盘是主要的外存。它完成第一个任务是没问题的,但完成第二个任务不能胜任,因为速度太慢了,起码慢100倍。于是只好用内存来代替它,完成第二个任务。
- 但内存虽然速度快,价格却非常贵,而且关机文件就丢失,也不能做成很大的容量,所以,还得硬盘来辅助。所以,就造成了内存结合外存的局面。
二、操作系统
什么是操作系统
操作系统由内核和应用组成,在计算机软硬件架构中,操作系统起到的是管理的作用,负责管理计算机的软硬件资源,也就是说,操作系统是一个稿管理的软件。
用户、操作系统、计算机硬件之间的关系
由于操作系统的内核过于脆弱,所以不允许用户从外部直接操作,于是开发人员在开发操作系统的时候,会给用户提供系统调用接口,用户通过系统调用接口来访问操作系统内核,完成一系列工作。
但是由于系统调用接口有的过于复杂,并不是人人都是程序员,并没有办法很好的使用这些系统调用接口,于是通过把系统调用接口进行封装,可以把多个复杂的系统调用接口封装成一个命令,于是就有了现在的用户操作接口,包括shell命令,库函数等。
用户就可以通过用户操作接口,输入操作指令,就可以实现对操作系统内核的调用。
计算机硬件是通过各自的驱动程序来实现对操作系统的访问的,硬件驱动会自动采集硬件信息,传输给操作系统,完成鼠标,键盘,硬盘等一系列操作。
图示:
三、进程概念
从用户角度来看:进程就是一个正在运行的程序。
从操作系统的角度来看:操作系统运行一个程序,需要描述这个程序的运行过程,在Linux下,这些描述信息杯保存在一个task_struct结构体中,每一个结构体中都保存着一个程序的运行信息,这些结构体统称为PCB(process control block),所以对于操作系统来说,进程就是PCB。
进程的描述信息都包括:进程的标识符PID,进程状态,进程优先级,程序计数器,上下文数据,内存指针,IO信息等。这些都由操作系统进行调度,实现对进程的有序管理。
通过对运行程序的描述,CPU就可以调度哪个程序去占用CPU去运行指令,要运行哪个程序,操作系统则找到对应程序的PCB,在PCB中取出程序运行所需要的信息,把他加载到CPU上,就可以运行这个程序了。
内核源代码路径:/usr/src/kernels/3.10.0-1160.45.1.el7.x86_64/include/linux/sched.h
查看进程
在Linux输入以下指令,来直接显示进程的状态。
ps -ef 或者ps -aux
创建进程
在LIinux中,进程就是一个PCB,就是一个task_struct结构体,创建一个进程,就是创建一个task_struct结构体。
Linux中创建子进程的函数——fork()
在Linux中,通过 fork() 这个函数来创建进程进程,这个接口是通过复制调用 fork() 接口的进程,创建一个新的进程。也就是说一个进程(被称为父进程),通过调用fork接口,创建了一个新的进程(子进程),子进程完全复制了父进程的内容。
通过代码理解一下
#include<stdio.h>
#include <unistd.h>
int main ()
{
fork(); //创建子进程
printf("hello world\n");
return 0;
}
运行的结果
可以发现hello world 被打印了两遍,这是因为,在父进程中,调用了fork接口创建了一个子进程,子进程完全复制的是父进程的信息,所以子进程运行了一遍printf函数,父进程也运行了一次printf函数。
如果我们在创建子进程前面再加上一个printf函数
#include<stdio.h>
#include <unistd.h>
int main ()
{
printf("hello\n");
fork(); //创建子进程
printf("hello world\n");
return 0;
}
可以发现 hello 只之打印了一遍,hello world 打印了两遍,通过这个运行结果对比我们可以知道,子进程虽然是完全复制的父进程的所有信息,但是子进程只会从调用 fork 这个接口之后的语句开始运行。
使用 getpid() 接口获取调用这个接口的额PID可以更直观的看出
#include<stdio.h>
#include <unistd.h>
int main ()
{
printf("hello\n");
fork(); //创建子进程
printf("hello world: %d\n",getpid()); //获取父、子进程的PID
return 0;
}
这两次输出所运行的printf函数并不是由同一个进程运行的。再使用ps -ef 可以查看这两个进程。
四、进程状态
进程状态主要是作用于操作系统对于进程的管理,让操作系统知道该进程处于什么样的状态,就会自动对不同状态的进程进行不同的操作。
Linux下的进程状态
- 运行态(R):正在运行,或者随时可以运行的进程,统称为运行态,只有属于运行态的进程才能够被操作系统调度在CPU上进行运行。
- 可中断休眠态(S):可以被终端的休眠态,在满足唤醒条件,或者休眠被中断的情况下可以进入运行态。
- 不可中断休眠态(D):不能被中断的休眠状态,就是说只有满足唤醒条件后才会进入运行态。
- 暂停态(T):程序停止运行,什么都不做。
- 僵尸态(Z):进程已经推出不在调度了,但是这个进程的资源还没有被完全释放,等待处理的一种状态。
代码演示:
可中断休眠态:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main ()
{
int a = 0;
sleep(5);
while (1)
{
a++;
}
return 0;
}
程序运行后我们可以查看当前进程
开始程序会进行5秒的睡眠,此时处于可中断休眠态(S),五秒后休眠状态被打断,进入运行态。
僵尸态:
处于僵尸态的进程,成为僵尸进程,是一种已经退出了,但是资源没有被释放的进程。
- 产生原因:子进程先于父进程释放,但是父进程没有关注到子进程的退出,在子进程退出后,在进程PCB中保存了自己退出的返回值,在父进程没有关注处理的情况下,PCB资源是不会被释放的,因此系统并不会完全释放子进程的全部资源,这个进程就成为了僵尸进程。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main ()
{
pid_t pid = fork();
if (pid < 0)
{
//创建子进程失败
}
else if(pid == 0)
{
//子进程运行以下代码,对于子进程来说,返回值是0
printf("子进程的PID:%d\n",getpid());
sleep(5);
exit(0);
}
else
{
//父进程运行一下代码,对于父进程来说,返回值是子进程的PID
printf("父进程的PID:%d\n",getpid());
}
while(1)
sleep(1);
return 0;
}
运行后我们发现,程序在前五秒睡眠期,属于可中断休眠态,五秒后,执行 exit 指令,子进程先于父进程推出,但父进程并没有关注到子进程的退出,释放子进程的资源,所以子进程成为僵尸进程。
孤儿进程:
父进程先于子进程退出,这个子进程就成为了孤儿进程,这个子进程的父进程成为1号进程,并且这个孤儿进程会一直运行在后台,不占据终端。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(char argc, char* argv)
{
pid_t pid = fork();
if(pid < 0)
{
printf("for error\n");
}
else if(pid == 0)
{
printf("child-%d",getpid());
sleep(15);
}else
{
printf("parents-%d\n",getpid());
sleep(3);
exit(0);
}
return 0;
}
在父进程退出后,子进程成为孤儿进程,父进程成为一号进程。