在上篇博客浅析进程(一)中,提到了进程的一些概念,以及与进程有关的一些内容。程序在执行时,加载到内存,由操作系统进行描述,然后组织成双链表,进而管理。而这个过程中由操作系统描述后的内容被叫做PCB,而在Linux操作系统下,PCB是一个名字叫做 task_struct的结构体,那这个结构体内到底是什么东西呢?
一、进程的调度算法
进程的调度算法是指:根据系统的资源分配策略所规定的资源分配算法。
进程调度算法有五种:
1. 时间片轮转调度算法(RR):给每个进程固定的执行时间,根据进程到达的先后顺序让进程在单位时间片内执行,执行完成后便调度下一个进程执行,时间片轮转调度部考虑进程等待时间和执行时间,属于抢占式调度。优点:兼顾长短进程。缺点:平均等待的时间过长,上下文切换较费时间。适用于分时系统。(分时系统:多个用户分享使用同一台计算机,多个程序分时共享软件和硬件资源。)
2.先来先服务调度算法(FCFS): 根据进程到达的先后顺序执行进程,不考虑等待时间和执行时间,会产生饥饿现象。属于非抢占式调度。优点:公平,实现简单。缺点:不利于短进程工作。(饥饿现象:等待时间过长给进程的推进和响应造成明显影响叫做饥饿现象)
3.优先级调度算法(HPF):在进程等待队列中选择优先级最高的来执行。
4.多级反馈队列调度算法:将时间片轮转与优先级调度结合,把进程按优先级分成不同的队列,先按优先级调度,优先级相同的按时间片轮转。优点:可以兼顾长短进程,有较好的响应时间,可行性强。适用于各种作业环境。
5.高响应比优先调度算法:根据“响应比 = (进程执行时间 + 进程等待时间)/ 进程执行时间”这个公式得到响应比来进行调度。高响应比优先算法在等待时间相同的情况下,进程执行的时间越短,其响应比越高,满足段任务优先, 同时响应比会随着等待时间增加而变大,优先级会提高,能够避免饥饿现象。优点:兼顾长短进程。缺点:计算机响应开销比较大。适用于批处理系统。
这里说一下抢占式调度与非抢占式调度的区别。
抢占式调度:允许将逻辑上可继续运行的进程,在运行过程中将其暂停,从而调度其他的进程。可以防止单一进程长时间独占CPU系统开销大。
非抢占式调度:让进程运行直到结束或阻塞的调度方式,容易实现,适合专用系统,不适合通用系统 。
二、task_struct结构体
我们知道,在Linux操作系统下,进程控制块即PCB是task_struct结构体,那这个结构体内有什么内容呢?
利用find命令,我们在usr的路径下最终找到了包含task_struct结构体的文件。
struct task_struct
{
//说明了该进程是否可以执行,还是可中断等信息
volatile long state;
//Flage 是进程号,在调用fork()时给出
unsigned long flags;
//进程上是否有待处理的信号
int sigpending;
//进程地址空间,区分内核进程与普通进程在内存存放的位置不同
mm_segment_t addr_limit; //0-0xBFFFFFFF for user-thead
//0-0xFFFFFFFF for kernel-thread
//调度标志,表示该进程是否需要重新调度,若非0,则当从内核态返回到用户态,会发生调度
volatile long need_resched;
//锁深度
int lock_depth;
//进程的基本时间片
long nice;
//进程的调度策略,有三种,实时进程:SCHED_FIFO,SCHED_RR, 分时进程:SCHED_OTHER
unsigned long policy;
//进程内存管理信息
struct mm_struct *mm;
int processor;
//若进程不在任何CPU上运行, cpus_runnable 的值是0,否则是1 这个值在运行队列被锁时更新
unsigned long cpus_runnable, cpus_allowed;
//指向运行队列的指针
struct list_head run_list;
//进程的睡眠时间
unsigned long sleep_time;
//用于将系统中所有的进程连成一个双向循环链表, 其根是init_task
struct task_struct *next_task, *prev_task;
struct mm_struct *active_mm;
struct list_head local_pages; //指向本地页面
unsigned int allocation_order, nr_local_pages;
struct linux_binfmt *binfmt; //进程所运行的可执行文件的格式
int exit_code, exit_signal;
int pdeath_signal; //父进程终止是向子进程发送的信号
unsigned long personality;
//Linux可以运行由其他UNIX操作系统生成的符合iBCS2标准的程序
int did_exec:1;
pid_t pid; //进程标识符,用来代表一个进程
pid_t pgrp; //进程组标识,表示进程所属的进程组
pid_t tty_old_pgrp; //进程控制终端所在的组标识
pid_t session; //进程的会话标识
pid_t tgid;
int leader; //表示进程是否为会话主管
struct task_struct *p_opptr,*p_pptr,*p_cptr,*p_ysptr,*p_osptr;
struct list_head thread_group; //线程链表
struct task_struct *pidhash_next; //用于将进程链入HASH表
struct task_struct **pidhash_pprev;
wait_queue_head_t wait_chldexit; //供wait4()使用
struct completion *vfork_done; //供vfork() 使用
unsigned long rt_priority; //实时优先级,用它计算实时进程调度时的weight值
//it_real_value,it_real_incr用于REAL定时器,单位为jiffies, 系统根据it_real_value
//设置定时器的第一个终止时间. 在定时器到期时,向进程发送SIGALRM信号,同时根据
//it_real_incr重置终止时间,it_prof_value,it_prof_incr用于Profile定时器,单位为jiffies。
//当进程运行时,不管在何种状态下,每个tick都使it_prof_value值减一,当减到0时,向进程发送
//信号SIGPROF,并根据it_prof_incr重置时间.
//it_virt_value,it_virt_value用于Virtual定时器,单位为jiffies。当进程运行时,不管在何种
//状态下,每个tick都使it_virt_value值减一当减到0时,向进程发送信号SIGVTALRM,根据
//it_virt_incr重置初值。
unsigned long it_real_value, it_prof_value, it_virt_value;
unsigned long it_real_incr, it_prof_incr, it_virt_value;
struct timer_list real_timer; //指向实时定时器的指针
struct tms times; //记录进程消耗的时间
unsigned long start_time; //进程创建的时间
//记录进程在每个CPU上所消耗的用户态时间和核心态时间
long per_cpu_utime[NR_CPUS], per_cpu_stime[NR_CPUS];
//内存缺页和交换信息:
//min_flt, maj_flt累计进程的次缺页数(Copy on Write页和匿名页)和主缺页数(从映射文件或交换
//设备读入的页面数); nswap记录进程累计换出的页面数,即写到交换设备上的页面数。
//cmin_flt, cmaj_flt, cnswap记录本进程为祖先的所有子孙进程的累计次缺页数,主缺页数和换出页面数。
//在父进程回收终止的子进程时,父进程会将子进程的这些信息累计到自己结构的这些域中
unsigned long min_flt, maj_flt, nswap, cmin_flt, cmaj_flt, cnswap;
int swappable:1; //表示进程的虚拟地址空间是否允许换出
//进程认证信息
//uid,gid为运行该进程的用户的用户标识符和组标识符,通常是进程创建者的uid,gid
//euid,egid为有效uid,gid
//fsuid,fsgid为文件系统uid,gid,这两个ID号通常与有效uid,gid相等,在检查对于文件
//系统的访问权限时使用他们。
//suid,sgid为备份uid,gid
uid_t uid,euid,suid,fsuid;
gid_t gid,egid,sgid,fsgid;
int ngroups; //记录进程在多少个用户组中
gid_t groups[NGROUPS]; //记录进程所在的组
//进程的权能,分别是有效位集合,继承位集合,允许位集合
kernel_cap_t cap_effective, cap_inheritable, cap_permitted;
int keep_capabilities:1;
struct user_struct *user;
struct rlimit rlim[RLIM_NLIMITS]; //与进程相关的资源限制信息
unsigned short used_math; //是否使用FPU
char comm[16]; //进程正在运行的可执行文件名
//文件系统信息
int link_count, total_link_count;
//NULL if no tty 进程所在的控制终端,如果不需要控制终端,则该指针为空
struct tty_struct *tty;
unsigned int locks;
//进程间通信信息
struct sem_undo *semundo; //进程在信号灯上的所有undo操作
struct sem_queue *semsleeping; //当进程因为信号灯操作而挂起时,他在该队列中记录等待的操作
//进程的CPU状态,切换时,要保存到停止进程的task_struct中
struct thread_struct thread;
//文件系统信息
struct fs_struct *fs;
//打开文件信息
struct files_struct *files;
//信号处理函数
spinlock_t sigmask_lock;
struct signal_struct *sig; //信号处理函数
sigset_t blocked; //进程当前要阻塞的信号,每个信号对应一位
struct sigpending pending; //进程上是否有待处理的信号
unsigned long sas_ss_sp;
size_t sas_ss_size;
int (*notifier)(void *priv);
void *notifier_data;
sigset_t *notifier_mask;
u32 parent_exec_id;
u32 self_exec_id;
spinlock_t alloc_lock;
void *journal_info;
};
注:此task_struct来源于网络。
三、孤儿进程与僵尸进程
在上篇博客中提到了僵尸进程与孤儿进程,在这里来模拟实现以下僵尸进程与孤儿进程。
僵尸进程:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
int ret = fork();
while(1)
{
if(ret)
{
printf("father:pid:%d, ppid:%d\n",getpid(),getppid());
sleep(1);
}
if(!ret)
{
printf("child:pid:%d, ppid:%d\n",getpid(),getppid());
sleep(1);
}
}
return 0;
我们先利用fork()来创建子进程父进程,父子进程一直死循环运行,然后利用kill来杀死这个子进程,此时子进程便成了僵尸进程。
孤儿进程:
通用利用上个代码,我们杀死父进程,然后再来观察。
这样我们就看到了,在父进程结束后,孤儿进程由一号进程领养,变为其父进程。
欢迎大家共同讨论,如有错误及时联系作者指出,并改正。谢谢大家!