第一部分 进程知识总结
一、进程的定义
进程是执行中的程序,就类比于一出舞台剧的整个表演过程;进程动态性的实质是进程实体的执行过程;进程独立性是指每个进程都有自己的PCB;进程的并发性是内存中可以允许存在多个进程实体,并且可以同时运行一段时间;
二、进程的PCB分配
内核为我们维护了一个task_list的双向循环链表,每个链表节点都是一个task_struct(进程描述符)类型,在inux系统下主要依靠slab分配器分配task_struct结构,这些task_struct结构存在于内核栈的尾部,通过栈指针可以搜索到他们的位置,在内核栈的尾部分配出一个thread_info结构体,这个结构体中存放了实际指向task_struct结构的指针;
每个PCB中存放了唯一标识进程的pid_t类型的pid号,实际上就是一个int类型;pid的默认最大值类型为一个short int 的最大值---32768;另外还存放了其他信息:
- 用于进程标识信息的进程标识符
内部标识符:由操作系统提供的唯一的进程序号;
外部标识符:用户创建的由数字和字母组成的字符串,便于创建者使用该进程;
通常如果有父子关系,还会设置ppid和pid来标识父子关系
- 用于进程处理机状态信息
通用寄存器:供用户程序访问暂存信息
指令寄存器:存放下一条指令的地址
程序状态字:含有状态信息条件码、执行方式、中断屏蔽标志
用户栈指针:存放过程和系统调用参数及调用地址,可以保存现场信息,下一次调度时可以继续执行
- 用于进程调度信息
进程的优先级 进程的状态
三、进程的状态
进程的主要状态有:就绪、阻塞、运行,三者之间的关系如图所示:
进程被替换的两种情况:
- 第一种为主动情况,该进程执行完自己的任务主动将CPU让出或者该进程执行有阻塞操作,操作系统将会挂起该进程执行新进程
- 第二种为被动情况,当一个进程被操作系统认为长时间占用CPU时会主动用另一进程替换正在运行的此进程;保证每个进程公平的占用CPU
- 上图中,进程不会由阻塞转为执行态:CPU每次进程调度是在就绪队列中选择进程运行,不会在阻塞队列中调度进程;(理解难点:当阻塞状态等待事件发生时进程是转为就绪态而不是直接转为执行态
- 上图中,进程不会由就绪-->阻塞态,由于就绪态的进程并没有执行就不会出现阻塞状态。
,四、进程的创建
linux下使用pid_t fork()系统调用函数创建进程,该函数一次执行,返回两次,在父进程中返回子进程的pid,在子进程中返回0;子进程和父进程的代码段完全相同,并且父子进程共享fork之前打开的文件描述符,当有子进程产生时,文件描述符的引用计数加1;
通常我们创建新进程是为了执行新的不同的程序,所以出现了进程替换,使用一组进程替换函数使得新进程执行新的程序;
五、处理僵尸进程
(1)僵尸进程
定义1:父进程未结束,子进程结束,并且父进程未获取子进程的退出数据;
定义2:一个进程的进程主体释放,而其PCB未释放;
(2)解决僵尸进程
本质是只要让父进程能得到子进程结束的信息后在结束
第一种方法:pid_t wait(int *stat):
阻塞运行:函数被调用后不会立即返回,等待某些条件的发生才会返回;
wait函数会阻塞运行;等待子进程结束才能返回,致使父进程阻塞到wait调用处;
wait函数的缺点:一个wait函数只能处理一个僵尸进程,在实际中无法预测到底有多少个僵尸进程,这就阻碍了我们能处理僵尸进程的能力,引入了异步处理方式:信号
第二种方法:信号
绑定信号及其响应方式,只要有子进程结束的信号就通知父进程。
具体做法:
当子进程结束时会发出SIGCHLD信号,但是一般处于默认操作,所以我们可以利用信号处理函数通过修改信号响应方式来向父进程传递子进程结束的状态数据;
修改的响应方式:当子进程结束时SIGCHLD信号通过绑定的fun函数调用wait函数来处,当不存在子进程结束时父进程照常运行;当存在子进程先于父进程结束时通过SIGCHLD信号通过fun函数告诉父进程有僵尸进程需要处理,比不需要之前父进程要一直等待子进程结束后才能运行,代码如下:
僵尸进程的坏处:从定义可得知,僵尸进程会占用PCB实体,但是并没有实际应用,造成PCB资源的浪费,如果存在过多的僵尸进程,会造成新进程因不能分配到PCB而无法执行。
六、处理孤儿进程
(1)孤儿进程
指得是当父进程结束时子进程还未结束,这时候孤儿进程的父进程就变为守护进程init;
(2)模拟孤儿进程 可以看到被init守护进程回收管理
七、守护进程
(1)守护进程
在后台运行,不与任何终端关联的进程,通常情况下守护进程在系统启动时就在运行,它们以root用户或者其他特殊用户(apache和postfix)运行,并能处理一些系统级的任务;
守护进程是完全脱离终端在后台运行的,脱离终端的目的是不让在终端被显示;
(2)创建守护进程
守护进程编程流程
1.首先调用umask()函数将文件模式创建屏蔽字置为0;
2.调用fork()然后父进程退出,这样做的目的:一是如果该守护进程作为一条简单的shell命令启动,那么父进程终止是使shell认为这条命令已经执行完成;二是子进程继承父进程的进程组ID,但是具有一个新进程的ID,这就保证了子进程不是一个组长进程。这是调用setid()的必要条件
3.调用setid()创建新的会话,这样使调用进程成为新会话的首进程,成为新进程的组长进程,没有控制终端
4.调用fork(),然后父进程退出,这样保证了进程不再是会话组长,从而不能打开一个新的终端
5.将当前工作目录改为根目录,调用chdir()实现
6.关闭不需要的文件描述符,可以通过getrlimit函数获得最大描述符值,关闭从该值到最大描述符值的所有文件描述符
7.打开“/dev/null”并且使其具有文件描述符0,1,2,这样任何一个试图通过标准输入、写标准输出或标准错误输出得到的库函数没有任何效果,因为守护进程不与终端关联。
编程代码如下:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
#include <sys/types.h>
#include <signal.h>
#include <sys/stat.h>
void Init_Daemon()
{
umask(0);
pid_t pid = -1;
if( (pid = fork()) < 0)
{
exit(0);
}
else if(pid != 0)
{
exit(0);
}
setid();
if((pid = fork()) < 0)
{
exit(0);
}
else if (pid != 0)
{
exit(0);
}
if(0 > chdir("/"))
{
exit(0);
}
struct rlimit rt;
if(getrlimit(RLIMIN_NOFILE,&rt) < 0)
{
exit(0);
}
int i = 0 ;
for(; i < rl.rlim_max,++i)
{
close(i);
}
int fd0 = open("/dev/null",O_RDWR);
int fd1 = dup(fd0);
int fd2 = dup(fd0);
}
若将此函数在main函数中运行,并且main函数进入睡眠状态,那么最终获得就是init进程。