操作系统
什么是操作系统
操作系统(operation system,简称OS),简单来说就是一款纯正的“搞管理”的软件,不仅管理硬件,同时也管理软件
- 与硬件交互,管理所有的软硬件资源
- 为用户程序(应用程序)提供一个良好的执行环境
操作系统主要包括一下两个:
- 内核(进程管理,内存管理,文件管理,驱动管理)
- 其他程序(例如函数库,shell程序等等)
操作系统如何管理
我们将操作系统、驱动程序、底层硬件分别比作校长、辅导员、学生
- 一个人能否进行管理,关键在于它是否具有决策权,正如校长一样,它可以决定你的去留。校长只需通知辅导员(驱动程序),就可以让你跑路(决策),辅导员则听从命令,执行命令
- 当校长想要了解某个学生的情况时,自然可以让辅导员将学生的相关信息交予给你,这就是操作系统调用驱动程序来了解硬件的过程
- 学生(硬件)这么多,如何统一进行管理呢?答案就是,信息的导入,将学生的信息包装起来,通过这些信息来管理学生,辅导员(驱动程序)则执行校长的决定
经上述的描述操作系统是如何进行管理的呢?很简单,用“先描述,再组织”的思想,如何描述?那就是把想要管理的信息用结构体装起来,组织则是用数据结构将他们一一串起来。
系统调用和库函数概念
我们刚刚只关心了操作系统、驱动程序、底层硬件,那往上看还有system call和用户操作接口,这又是用来干什么的呢?
- 在开发角度,操作系统对外会表现为一个整体,但是会暴露自己的部分接口,供上层开发使用,这部分由操作系统提供的接口,叫做系统调用。
- 系统调用在使用上,功能比较基础,对用户的要求相对也比较高,所以,有心的开发者可以对部分系统调用进行适度封装,从而形成库,有了库,就很有利于更上层用户或者开发者进行二次开发。
简单的例子就是printf 函数就是用户的二次开发,以此调用系统调用接口已完成相应的功能,为什么不直接使用系统调用接口呢?使用成本太大,使用过程需要了解更多相关知识。
为什么需要操作系统
在一套系统中,需要有管理者进行统筹。对上,给用户一个稳定高效的执行环境。对下,管理好软硬件资源,提供稳定的软硬件环境。
进程
先来看一个程序
1 #include <stdio.h>
2 #include <unistd.h>
3 #include <sys/types.h>
4 int main()
5 {
6 while(1)
7 {
8 printf("hello world: %d %d\n", getpid(), getppid());
9 sleep(1);
10
11 }
12 return 0;
13
14 }
当使用make/makefile生成一个可执行程序,这个程序一开始是会存储在硬盘上,然后当我们使用它时./test.exe ,就会把程序加载到内存中,所以开始运行后使用相关命令查找到这个进程。
进程的概念
进程说的难懂一点就是:可执行程序与管理进程所需要的数据结构的集合
通俗一点,像上述程序的一个执行实例,或者说正在执行的程序都叫做进程
我们可以通过/proc 系统文件进行查看,或者使用ps aux | grep [目标进程] 进行查看。
如何管理进程
一、描述
前文说过,操作系统主要有四大功能:内存管理,进程管理,文件管理和驱动管理。对于操作系统,只要是管理就遵从先描述,再组织的原则
每个进程的相关信息封装在一个struct中(因为Linux是由C/C++编写的),接着把这些结构体用我们学习的数据结构织起来,进行管理时只需遍历他们,然后修改相应的信息。
二、PCB
- 进程信息被放在一个叫做进程控制块的数据结构中,可以理解为进程属性的集合。
- 课本上称之为PCB(process control block),Linux操作系统下的PCB是: task_struct
三、task_struct
task_struct——是在Linux中描述进程的结构体,它是Linux内核的数据结构,会被装在到内存里面。
如下:
| |
---|
标识符 | 描述本进程的唯一标识符,类似于身份证 | 状态 | 任务状态,退出代码,退出信号 | 优先级 | 相对于其他进程的优先级 | 程序计数器 | 程序中即将被执行的下一条指令地址 | 内存指针 | 包括程序代码和进程相关数据的指针等 | 上下文数据 | 进程执行时处理器的寄存器中的数据 | I/O状态信息 | 包括显示的输入输出请求等 |
进程相关操作
一、查看进程
/proc ,Linux系统中所有的进程会被镜像在此。ps aux | grep [进程名] && ps aux | head -1 ,将所需要查看的进程内容以及相关目录调出来ps axj 查看父子进程关系
二、进程与父进程
1 #include <stdio.h>
2 #include <unistd.h>
3 #include <sys/types.h>
4 int main()
5 {
6 while(1)
7 {
8 printf("hello world: %d %d\n", getpid(), getppid());
9 sleep(1);
10
11 }
12 return 0;
13
14 }
- 使用getpid()获取进程标识符
- 使用getppid()获取父进程标识符
将19681调出来可得到
这里的-bash 叫做命令行解释器,所有命令都是由它创建的,那么自然而然它的PID就不会变化。前面的程序的执行也是依靠它的,命令行解释器要是挂了,系统也就完了。
其实这里的命令行解释器也不是其本体,因为命令行解释器特别重要,所以命令行解释器只需创建它的子进程,让这个子进程代替自己完成任务,即便子进程挂了,也不会影响其自身。当然不是所有的操作都能有子进程完成,有些特殊操作还需要本体亲自出马
fork-进程创建
fork函数的提出
- 在fork函数执行后,如果成功创建新进程就会出现两个进程,一个是子进程,一个是父进程
- fork有两个返回值
- 父子进程代码共享,数据各自开辟空间,私有一份(采用写时拷贝)
事例如下:
1 #include <stdio.h>
2 #include <sys/types.h>
3 #include <unistd.h>
4
5 int main()
6 {
7 printf("output once\n");
8 int ret=fork();
9
10 printf("output twice : pid: %d, ppid: %d, ret = %d\n", getpid(), getppid(), ret);
11 sleep(1);
12 return 0;
13
14 }
运行结果:
可以发现fork()函数调用之后,多了一个子进程5137,且由父进程5136所创建。
接下来我们将具体fork函数展示出来(官方手册 man fork )
- 两个返回值
- 在父进程中,fork返回新创建子进程的ID
- 在子进程中,fork返回0
- 未能创建,fork返回负值
根据上述信息,我们将其代码编写出来,用fork函数的返回值来进行分流
1 #include <stdio.h>
2 #include <sys/types.h>
3 #include <unistd.h>
4
5 int main()
6 {
7 pid_t ret = fork();
8 if(ret > 0)
9 {
10 while(1){
11 printf("I am an parent! pid : %d\n", getpid());
12 sleep(1);
13 }
14 }
15 else if(ret == 0){
16 while(1){
17 printf("I am a child! pid : %d, ppid : %d\n", getpid(), getppid());
18 sleep(1);
19 }
20 }
21 else{
22 printf("fork error\n");
23 }
24
25 return 0;
26 }
展示效果如下:
同时再使用之前的命令查看这个进程,发现也是两个进程 我们已知c语言,if-else 执行时每次只能执行一路,怎么可能同时执行多路,同时每个if语句块内都有死循环,一个循环未结束,又怎么可能去执行其他语句呢?
解释:
在Linux中,进程创建会形成链表,父进程创建子进程,那么父进程的进程指针会指向子进程ID。
所以这两个进程是同时运行的
fork相关问题
如何理解进程创建?
前面说过,操作系统在进行管理时,必然遵循“先描述,再组织”的原则,所以在进行进程管理时。首先会创建相应的task_struct ,写入有关信息,然后和你编写好的代码共同组成进程
fork拥有两个返回值的原因
根据上面的描述,可以大致描述fork函数的执行逻辑如下
pid_t fork()
{
struct task_struct* p=malloc(struct task_struct);
p->XX=father->XX;
....
p->status=run;
p->id=xxxx;
return p->id;
}
进程数据=代码+数据,代码是共享的,但是数据子进程会从父进程那儿拷贝一份(写时拷贝),在进行return 语句时,子进程已完成拷贝,于是两个进程共同return,但是数据不同导致返回值有所差异。
进程数据=代码+数据,代码是共享的,
**这两个进程都是独立的,存在于不同地址中,不是公用的。**也就是说明一个问题,分流的两条支路独立,互不干扰。
为什么返回值不一样
指向不一样,父进程指向子进程,所以返回的是子进程的id,但是子进程并没有它的子进程,所以返回0。
就好比,一个孩子肯定知道它只有一个爹,而一个爹可能有多个孩子,所以子进程在标识父进程时就不要做那么多的区分,但是父进程可能有多个子进程,它与它在区分不同的子进程时必须要使用PID。
为什么数据私有,代码却公有
- 代码是逻辑,一般不可修改
- 数据可读可写,方便讲两个进程独立开
- 如果数据不私有,后果就是同一份数据在父子进程之间改来改去引起混乱
进程状态
进程状态反映进程执行过程的变化。这些状态随着进程的执行和外界条件的变化而转换
Linux源代码中定义进程状态如下
* 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)", -运行或将要运行
"S (sleeping)", -进程在等待事件完成
"D (disk sleep)", -此状态进程通常等待IO结束
"T (stopped)", -停止状态
"t (tracing stop)", -追踪中
"X (dead)", -死亡状态
"Z (zombie)", -僵尸进程
};
状态讲解
R(running)
先看死循环程序
1 #include <stdio.h>
2
3 int main()
4 {
5 while(1){
6 printf(".");
7
8 }
9 return 0;
10 }
运行结果:
问题的抛出:
循环在不断打印,为什么进程状态是S呢。(即为S(sleeping)-睡眠状态)
其实刚才的这样的操作属于I/O操作,字符被不断打印在屏幕上,外设的速度是远低于CPU的,所以CPU早都处理完了,但是屏幕上的还没有打印完,所以这里显示的是S。
1 #include <stdio.h>
2
3 int main()
4 {
5 while(1){
6
7
8 }
9 return 0;
10 }
在这里插入图片描述
当去掉打印函数后,程序运行就不需要再打印再屏幕上,处理速度大大提升,这样结果就会有所不同,即变成R+状态。
结论:运行状态并不意味着进程一定在运行当中,它表明进程要么是在运行,要么在运行队列里,等待CPU调度
- 关于
S+ ,R+ ,其中的‘+’ 表示该进程是一个前台进程,可以使用ctrl+C 终止。在运行程序时,加上取地址符& ,比如./test & ,能使进程到后台运行,后天运行的进程无法使用ctrl+C 终止,必须使用命令kill -9 【进程pid】 来终止
S(sleeping)-睡眠状态
睡眠状态意味着进程在等待事件完成,处于等待队列或阻塞队列中。上面的死循环的例子就是典型的睡眠状态。睡眠状态可以被立即唤醒
D(Disk sleep)-磁盘休眠状态
磁盘休眠状态和睡眠状态有点像,区别就是睡眠状态可被中断,也就是立即唤醒,但磁盘休眠状态不可被中断,在这个状态的进程通常会等待I/O结束。
磁盘休眠状态又可以叫做深度睡眠状态
与S状态不同的是,D状态可以防止进程再等待磁盘数据是被杀死,为什么会被杀死,一是内存空间不足,二是因为磁盘搜寻速度太慢,当内存中的进程需要访问磁盘数据时,磁盘就立马去寻找,由于时间较慢,所以进程就会进入等待状态,如果进程是睡眠状态,而此时假如内存又不足了,CPU就需要终止某些进程以保证系统的稳定性,此时此进程就有可能会被误删,这样等磁盘拿到数据时,进程早已挂掉,因此会引发一定的问题。
T(stopped)-停止
man kill 查看相关kill命令的选项,我们使用命令kill -l ,查看信号(前31为普通信号,剩余部分为实时信号)- 我们使用18 19两个选项,分别对应R状态与T状态
进程状态路线图
僵尸进程
何为僵尸进程
简单点来说:僵尸进程就是子进程已经退出了,父进程还在运行当中,父进程没有读取到子进程的状态,子进程就会进入僵尸状态
例子
1 #include <stdio.h>
2 #include <sys/types.h>
3 #include <unistd.h>
4
5 int main()
6 {
7 pid_t ret = fork();
8 if(ret > 0)
9 {
10 while(1){
11 printf("I am an parent! pid : %d\n", getpid());
12 sleep(1);
13 }
14 }
15 else if(ret == 0){
16 int count = 0;
17
18 while(count < 5){
19 printf("I am a child! pid : %d, ppid : %d\n", getpid(), getppid());
20 sleep(1);
21 count++;
22 }
23 }
24 else{
25 printf("fork error\n");
26 }
27
28 return 0;
29 }
可以发现,在5秒后,子进程已经退出,父进程仍在运行 当子进程先退出,父进程还在运行,由于读取不到子进程的退出状态,所以子进程会变为僵尸状态。为了方便演示,使用下面的脚本,来每1s监控进程
while :; do ps axj | head -1 && ps axj | grep test | grep -v grep;sleep 1;echo "###########";done
父进程还是在运行,此时子进程变为Z,也就是僵尸状态
系统建立僵尸进程的原因
其实道理也很简单,子进程是由父进程创建的,父进程之所以要创建子进程,其目的就是要给子进程分配任务,那么在这个过程中,子进程平白无故的没了,而父进程却不知道子进程到底把自己交给它的任务完成的怎么样,成功了还好,失败的话就能再交代一个进程去操作。 所以进程结束时一定要给父进程返回一个状态,父进程一直不读取这个状态的话,那么子进程就会一直卡在僵尸状态,其中像代码这些资源已经被释放,但是这个进程却没有真正退出,因为PCB还在维护它,直到父进程读取到它的状态,才能进入死亡状态
- 进程控制块中,一个进程退出后,还有一个退出码返回给父进程,如下是Linux内核中关于这部分的定义
孤儿进程
孤儿进程就是父进程没了,子进程还在。那么根据上面的僵尸进程,子进程在退出后由于没有父进程来读取它的状态,所以会一直卡在僵尸状态,那么这样就会存在一个问题,它的内存资源谁来回收,通俗点将就会造成
内存泄漏
事例:父进程先挂
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
pid_t ret=fork();
sleep(1);
if(ret>0)
{
int cout=0;
while(cout<10)
{
printf("----------------------------------------------------\n");
printf("父进程运行了%d秒\n",cout+=1);
sleep(1);
}
exit(0);
}
else if(ret==0)
{
int count=0;
while(1)
{
printf("子进程已经运行了%d秒\n",count+=1);
sleep(1);
}
}
else
printf("进程创建失败\n");
sleep(1);
return 0;
}
ctrl+C此时结束的是父进程,但是父进程早已结束,子进程像孤儿一样四处游荡
用kill命令能够清除
那么问题来了,这个进程难道一直要占用资源吗,其实操作系统在设计的时候就考虑到了这一步。所以一旦父进程先挂了,那么这个子进程就会被1号进程领养(systemd)
|