多任务处理机制
首先认识一下:多道程序设计
计算机技术还不发达的时代,计算机每次只能处理单个任务,其他任务只能等待当前任务处理完成后才能被处理。
个人理解:臂如智能手机刚兴起阶段,就已经有多任务了,但是有很多人发现杀后台现象十分严重,切换到后台的应用程序过一两分钟再切换回来时,还需要重新加载进入。这是由于CPU在处理多任务的时候为了让前台的任务流畅的执行,把后台的任务给杀掉,这样就能保证前台的应用程序能够更多的获得时间片,从而提高使用的流畅度。
- 早期单道批处理系统,内存中仅有一道作业,该作业独占系统全部资源,致使系统的性能较差。
- 为了进一步提高资源利用率和系统吞吐量,上世纪60年代中期,引入多道程序设让技术并由此形成了多道批处理系统。其且的就是充分要利用系统的所有资源且尽可能的让它们并行。
程序的顺序执行
- 程序的顺序执行是指CPU严格按照程序的指令顺序执行。
- 在现代计算机系统中程序完全的顺序执行是不可能实现。因为,只有在单用户系统、无中断的情况下,程序的顺序执行才有可能。
/*
A.顺序性:上一条指令执行结束是下一条指令执行开始的充要条件。
B.封闭性:程序一旦开始运行,结果由给定的初始条件决定,不受外界影响。
C.可再现性:只要采用同样的初值,就能得到相同的结果。
D.确定性:程序的运行结果与执行的时间、速度、次数无关。
*/
程序的并发执行
- 程序的并发执行是让多个逻辑上相互独立的程序在计算机中交替执行,从而增强计算机的处理能力,提高系统资源的利用率。
- 程序的并发执行不再具有顺序执行时的封闭性和可再现性,而是出现了一些新的特征。
/*
A.间断性:并发程序都具有“走走停停”的特征,失去了原有的时序关系。
B.非封闭性:系统资源归所有并发程序共享,致使程序的运行环境相互影响,从而失去了封闭性。
C.不可再现性:由于并发执行,打破了封闭性,因而破坏了可再现性。
D.通信性:并发程序为了有效地协调共享资源的使用,相互之间需要通信。
*/
时间片轮转
Def:时间片轮转调度是一种最古老,最简单,最公平且使用最广的算法,又称RR调度。每个进程被分配一个时间段,称作它的时间片,即该进程允许运行的时间。
程序执行的两种方式,单任务和多任务
任务
任务就是一个程序执行一次的过程。一个任务可能会包含多个子任务。这些子任务就是进程或者线程。所以一个任务 里面可能会有多个进程或者多个线程。
进程:
进程是指一个具有独立功能的程序在某个数据集合上的一次动态执行过程。它是操作系统进行资源分配和调度的基本单位。
任务的特点 | |
---|---|
并发性 | 系统中可以同时运行多个进程,相互之间不受干扰 |
动态性 | 每一个进程都有生命周期,在进程的生命周期内,进程的状态是不断变化的 |
交互性 | 进程在执行的过程中可能会与其他的进程进行直接或间接的通信,臂如进程的同步与互斥,所以需要添加一定的进程处理机制 |
独立性 | 进程是OS进行资源分配和调度的基本单位,各个进程的地址空间相互独立,只有采用某些特定的机制才能实现各个进程间的通信。 |
进程的状态及转换
任何一个事物都有它的生命周期,进程也不例外。一个进程的生命周期可以划分为不同的状态,而且各种状态之间是可以转换的。一个进程由创建产生,因调度执行,因得不到资源而等待,由撤销而消亡。一般而言,它至少具备三个基本状态:就绪态、运行态、等待态。
就绪态(ready) | 处于处理机调度队列中的进程,已经准备就绪,一旦得到处理机,就立即可以运行。也就是说拥有处理机之外的一切必要资源的进程所处的状态 |
---|---|
运行态(running) | 当进程被调度选中,获得CPU控制权,其程序正在处理机上运行的状态。 |
等待态(blocked) | 若一个进程因为等待某一事件发生(如等待I/O操作的完成)而暂时无法继续执行的状态,也就是说进程除了处理机之外还有其它资源没有得到满足。 |
三态模型
- 就绪态→运行态:CPU空闲时,进程调度程序选中一个处于就绪态的进程占用处理机。
- 运行态→就绪态:运行进程的时间片用尽;或在可抢占的调度方式中,有更高优先级的进程处于就绪态。
- 运行态→等待态:运行进程因等待某种资源或某事件的发生而无法运行。
- 等待态→就绪态:进程等待的资源得到满足或某事件完成。
Linux下进程的几种状态
-
运行态:R 此时进程正在运行,或者准备运行(就绪态)
-
等待态 此时进程在等待一个事件的发生或某种资源
可中断等待态, S进程在等待事件完成(浅度睡眠,可以被唤醒)
不可中断等待态: D (不能被唤醒,通常在磁盘写入发生) -
停止态 :T 此时进程被中止
-
死亡态 X 该状态是返回状态,在任务列表中看不到
进程组
进程组:也称之为作业,BSD于1980年前后向Unix中增加的一个新特性,代表一个或多个进程的集合。
每个进程都属于一个进程组。在waitpid函数和kill函数的参数中都曾使用到。操作系统设计的进程组的概念,是为了简化对多个进程的管理。
当父进程,创建子进程的时候,默认子进程与父进程属于同一进程组。进程组ID为第一个进程ID(组长进程)。
组长进程: 可以创建一个进程组,创建该进程维的进程,然后终止。只要进程组中有一个进程存在,进程组就存在,与组长进程是否终止无关。
进程组生存期: 进程组创建到最后一个进程离开(终止或转移到另一个进程组).个进程可以为自己或子进程设置进程组ID
.
会话
会话是一个或多个进程组的集合。
- 一个会话可以有一个控制终端。这通常是终端设备或伪终端设备;建立与控制终端连接的会话首进程被称为控制进程;
- 一个会话中的几个进程组可被分为一个前台进程组以及一个或多个后台进程组;
- 如果一个会话有一个控制终端,则它有一个前台进程组,其它进程组为后台进程组;
- 如果终端接口检测到断开连接,则将挂断信号发送至控制进程(会话首进程)。
创建会话的注意事项
- 调用进程不能是进程组组长,该进程变成新会话首进程(session header)
- 该调用进程是组长进程,则出错返回
- 该进程成为一个新进程组的组长进程
- 需有root权限(ubuntu不需要)
- 新会话丢弃原有的控制终端,该会话没有控制终端
- 建立新会话时,先调用fork()函数,父进程终止,子进程调用setsid()函数
守护进程
守护进程(Daemon Process),也就是通常说的Daemon进程(精灵进程),是Linux中的后台服务进程。
它是一个生存期较长的进程,通常独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。
如果希望某个进程不因为用户、终端或者其他的变化而受到影响,那么就必须把它设置为一个守护进程。
编写守护进程的步骤
1:创建子进程,父进程退出,形成孤儿进程
fork()函数。通过man命令可以看到fork()函数的组成。
函数的返回值类型为pid_t类型。
返回值:由于fork()函数的功能是在当前进程中创建了一个新的进程,所以会有两个返回值。
返回值 | 代表的对象 |
0 | 父进程 |
>0 | 子进程的PID(进程号) |
子进程创建成功后,关闭父进程
由于父进程被关闭,所以此时子进程已经沦为孤儿进程,他的父亲就会变为Linux进程树中的1号进程:Init进程,由于我使用的虚拟机是64位Kali,所以用pstree命令得到的结果是systemd,在64位Ubuntu里面显示的也是systemd。但是在32位的linux系统中1号进程就是Init进程。
代码演示:
/*守护进程的创建步骤*/
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<fcntl.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<unistd.h>
int main()
{
pid_t pid = -1; //定义一个数据类型为pid_t类型的变量,名为pid.
pid = fork(); //使用fork()函数创建一个子进程
//判断是否创建成功?
if(0 > pid)
{
perror("fork error!\n"); //打印错误提示语句并说明错误类型!
exit(1); //exit(0)代表正常退出,1则表示异常退出
}
else if(0 < pid) //由于父进程返回值是子进程的PID号,所以必须>0
{
exit(0); //父进程退出
}
//当程序运行到此处就表示孤儿进程创建成功。接下来继续将其一步步变为守护进程
2:创建新的会话
前面已经说过什么是会话,不再重复。在这里主要说一下为什么要创建新的会话?
由于子进程是父进程通过fork()函数生成的,而fork()函数会将父进程的会话期、进程组、控制终端等全盘复制给子进程,所以即便我们让父进程退出了,但是此时子进程还是属于父进程的会话组,并未完全独立。
所以此时需要:
让子进程摆脱原父进程会话组的控制 |
让子进程拜托原父进程的进程组控制 |
让子进程摆原脱父进程控制终端的控制 |
setsid()函数
头文件:#include<sys/types.h>
#include<unistd.h>
函数原型:pid_t setsid(void);
返回值:成功,返回进程组的ID;失败返回-1。
通过man手册查看setsid()函数:
代码演示
/*守护进程的创建步骤*/
pid_t pid = -1; //定义一个数据类型为pid_t类型的变量,名为pid.
pid = fork(); //使用fork()函数创建一个子进程
//判断是否创建成功?
if(0 > pid)
{
perror("fork error!\n"); //打印错误提示语句并说明错误类型!
exit(1); //exit(0)代表正常退出,1则表示异常退出
}
else if(0 < pid) //由于父进程返回值是子进程的PID号,所以必须>0
{
exit(0); //父进程退出
}
//当程序运行到此处就表示孤儿进程创建成功。接下来继续将其一步步变为守护进程
setsid(); //为孤儿进程创建新的会话
3:改变当前目录—chdir()函数
由于fork()函数创建的子进程的工作路径式继承于父进程的工作路径,而我们的守护进程一般都运行于Init/systemd目录之下,所以需要改变子进程的工作路径,将其路径更改到我们既定的目录下。
chdir()函数:改变工作目录
通过man手册可以看到该函数的原型和描述,改变当前应用程序的工作路径。该函数传入的参数是一个地址,所以形参为一个指针类型的变量。
所以,要想改变当前的孤儿进程的工作路径,这就需要使用chdir()函数,将我们既定的路径传给chdir函数,完成修改。
这里常用的路径为根目录下的tmp文件夹下。有同学就会问:为什么要放在这个文件夹下?直接放在根目录不行吗? 当然可以啊。只不过需要看看Linux的文件系统层次结构后再决定放在那里。
Linux文件层次结构标准
不难看出,tmp目录是用来存放临时文件的,由于我们在学习的过程中不需要将自己写的守护进程每次开机就启动,所以更多的是将守护进程放在这个临时文件夹中,等虚拟机关机时就会自动清空文件夹。
代码演示:
int main()
{
pid_t pid; //定义一个子进程的pid号,类型为pid_t
int i , fd; //定一个文件描述符和i
char *buf = "This is a Daemon!\n";
/*1:创建子进程*/
pid = fork(); //fork函数,创建子进程
//1.1判断是否创建成功
if(0 > pid)
{
perror("fork error!\n");
exit(1);
}
//2.关闭父进程,形成孤儿进程
else if(0 > pid)
{
exit(0); //父进程退出
}
/*2:孤儿进程摆脱原来会话,进程组,控制终端的控制*/
setsid();
/*3:由于是守护进程,所以尽量将文件路径更改为根目录*/
chdir("/tmp"); //'/tmp'是linux存放临时文件的目录,关机后会清除里面的内容
4:改变文件权限掩码—umask()函数
Linux下,一切皆文件。但是文件又分为8类,分别是:
1.普通文件
2.目录文件
3.设备文件
3.1块设备文件:
3.2字符设备文件:
4.链接文件
5.管道文件
6.套接字文件
这些文件都有三种权限属性:可读、可写、可执行。不同类型的文件操作权限也不同。举例来讲,我们使用vim命令创建一个.c文件,然后通过ls -l命令来查看这个文件的详细属性,就会看到下面这种效果:
注意图中黄色框框的内容:
三个为一组组看:
1.第一段表示文件所有者对此文件的操作权限
2.第二段表示文件所有者所在组对些文件的操作权限
3.第三段表示除上述两种外的任何用户/组对此文件的操作权限
r | w | x | r | w | x | r | w | x |
---|---|---|---|---|---|---|---|---|
read | write | execute | read | write | execute | read | write | execute |
所以刚才创建的这个.c文件对文件所有者有读写权限,对于其他用户只有读权限。
回到子进程这里,我们通过fork()函数创建的子进程,全盘继承了父进程的文件权限,所以这可能会给该子进程使用文件造成一定的影响,因此需要修改文件权限掩码。
具体实现的原理是,使用三个8进制数字,每个数字通过8进制表示出来正好对应 r w x 三个文件属性,
举个栗子:
chdir(050); // 0 0 0 1 0 1 0 0 0
r w x r w x r w x
//对应的就屏蔽掉了组内成员对于此文件的读和执行的权限
代码演示:
int main()
{
pid_t pid; //定义一个子进程的pid号,类型为pid_t
int i , fd; //定一个文件描述符和i
char *buf = "This is a Daemon!\n";
/*1:创建子进程*/
pid = fork(); //fork函数,创建子进程
//1.1判断是否创建成功
if(0 > pid)
{
perror("fork error!\n");
exit(1);
}
//2.关闭父进程,形成孤儿进程
else if(0 > pid)
{
exit(0); //父进程退出
}
/*2:孤儿进程摆脱原来会话,进程组,控制终端的控制*/
setsid();
/*3:由于是守护进程,所以尽量将文件路径更改为根目录*/
chdir("/tmp"); //'/tmp'是linux存放临时文件的目录,关机后会清除里面的内容
/*4:设置文件掩码umask()*/
umask(0); //文件掩码为0,表示可以:读.写.执行
5:关闭文件描述符
子进程被创建的同时,父进程打开的文件,子进程也会继承。但是我们创建守护进程可能用不到那些打开过的文件,所以需要将其关闭以节省系统占用资源。
这里需要介绍一种函数:getdtablesize(),使用man手册查看该函数介绍:
这里说该函数的返回值类型为int型,参数为空。它会返回一个进程可以打开的最大文件数,比文件描述符的最大可能值多一个。
也就是说,通过这个函数的返回值,我们就能知道该进程中可能打开的文件数的最大值,然后我们将其全部关闭。
代码演示
/*close*/
for(i = 0 ; i < getdtablesize() ; i++)
{ //使用for循环将文件全部关闭
close(i);
}
至此,我们已经搭建好了守护进程框架,接下来就是让他实现特定的功能了。
这里演示的是:每隔两秒会向程序日志中写入一串字符串,【将原有的字符串清空,再添加新的字符串】
完整代码展示
#include<stdio.h> //标准IO库函数
#include<stdlib.h> //标注库函数
#include<string.h> //字符串库函数
#include<fcntl.h> //文件描述符库函数
#include<sys/types.h>//提供pid_t的定义
#include<unistd.h> //geipid就在里面
#include<sys/wait.h>
int main()
{
pid_t pid; //定义一个子进程的pid号,类型为pid_t
int i , fd; //定一个文件描述符和i
char *buf = "This is a Daemon!\n";
/*1:创建子进程*/
pid = fork(); //fork函数,创建子进程
//1.1判断是否创建成功
if(0 > pid)
{
perror("fork error!\n");
exit(1);
}
//2.关闭父进程,形成孤儿进程
else if(0 > pid)
{
exit(0); //父进程退出
}
/*2:孤儿进程摆脱原来会话,进程组,控制终端的控制*/
setsid();
/*3:由于是守护进程,所以尽量将文件路径更改为根目录*/
chdir("/tmp"); //'/tmp'是linux存放临时文件的目录,关机后会清除里面的内容
/*4:设置文件掩码umask()*/
umask(0); //文件掩码为0,表示可以:读.写.执行
/*close*/
for(i = 0 ; i < getdtablesize() ; i++)
{
close(i);
}
/*------------------------------------------------------------*/ /*创建完成,开始进入守护进程的配置*/
while(1)
{
int fd = -1;
fd = open("daemon.log" , O_WRONLY | O_CREAT | O_TRUNC); //打开或创建一个daemon日志文件
//判断是否打开成功
if(0 > fd)
{
perror("open error!\n");
exit(1);
}
//向日志中写入buf中的字符串
write(fd , buf , strlen(buf));
//关闭文件描述符
close(fd);
sleep(2);
}
exit(0);
}