操作系统——进程的概念


前言: 进程是什么?为什么有进程?怎样管理进程?这是本篇文章要搞定的问题。同学们注意了,鸡汤来喽!!!


1.进程的概念

1.1 进程初始

操作系统只认识二进制文件,所谓程序就是一个二进制文件,操作系统要运行程序时,该怎么操作呢?这就需要进程:程序被触发后,执行者的权限与属性,程序的代码与所需数据都会被加载到内存中,这个过程就是进程。
进程:程序代码+相关数据集
加载进内存该如何管理呢?操作系统需要直接面向加载到内存中的代码嘛?答案是不需要。
举个例子:
操作系统(校长),老师(进程控制块),学生(程序)。校长想要了解某个学生的学习情况,会去直接找学生嘛?一般不会,会去找对应的老师,向她问候一下学生情况,结果表示学生的成绩好,可以考虑保送(优先级高),校长说那就保送吧(开始执行),校长通过老师把学生给办了。

1.2 进程控制块—test_struct

所以每个程序在进入内存时,操作系统会自动创建一个PCB结构体来存放进程信息,Linux操作系统的PCB是task_struct,大家可能好奇程序代码也存在task_struct中嘛?task_struct存信息,它存的是指向程序代码的指针,程序代码是加载到内存中的。
在这里插入图片描述
可以看下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;
 };

内容很多,学习成本较大,这里简单讲讲其中的内容:

标示符: 描述本进程的唯一标示符,用来区别其他进程。
状态: 任务状态,退出代码,退出信号等。
优先级: 相对于其他进程的优先级。
程序计数器: 程序中即将被执行的下一条指令的地址。
内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
上下文数据: 进程执行时处理器的寄存器中的数据[休学例子,要加图CPU,寄存器]。
I/O状态信息: 包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。
记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。
其他信息;

这些内容在下面的学习中,都会遇见,倒是在细讲。

1.3 进程组织

进程信息由进程控制块存放,操作系统直接查看进程控制块就可以做出相应的操作。还有个问题:就是多个进程之间如何管理呢?谁先谁后?
多个进程控制块,用双向链表的形式存着,每个都是一个节点,这样就管理起来了,可以更据优先级信息等来判断,那个先执行等。


2.人为管理进程的操作

2.1 查看进程

(1)进程信息可以通过 /porc系统文件夹查看,具体查看那个进程,需要得知其标识符(PID)。
看到以下有很多的数字还是蓝标的,数字就是PID,蓝标说明这是个目录,比如接下来我想看看PID为1286的进程信息。
在这里插入图片描述
这个信息查看的不够银杏,所以可以带上 -l选项查看详细信息
在这里插入图片描述
详细了不少,但是也许只能看懂一个exe可执行文件。
在这里插入图片描述
(2)用ps工具来查看
关于ps工具,咱们只需要记住,只查看自己的bash进程选项为 -l,查看系统所有运行的进程选项为aux,这个不加-
在这里插入图片描述
这里可以看到,标识符PID展示出来了,还有一个PPID,这是父进程的PID,我们惊奇的发现,下面的ps指令的PPID是上面bash的PID。
在这里插入图片描述
命令行上的命令的父进程基本上都是bash。

2.2 进程标识符的获取
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
 int main()
{
 //getpid()获取进程标识符
 printf("pid: %d\n", getpid());
 //getppid()获取父进程标识符
 printf("ppid: %d\n", getppid());
 return 0;
}

在这里插入图片描述

2.3 创建进程fork()

利用fork()可以创建子进程。

  1 #include <stdio.h>
  2 #include <sys/types.h>
  3 #include <unistd.h>
  4 int main()
  5 {
  6    fork();                                                                                                                                                                             
  7    printf("hollow fork\n");
  8    sleep(1);
  9  return 0;
 10 }

看看运行结果:
在这里插入图片描述
惊奇的发现,hollow fork被打印了两遍。这就有些奇怪了。因为fork()创建了子进程,俩个进程所以会打印俩次。为什么两个进程会打印俩次呢?

默认情况下:子进程会继承父进程的代码和数据,包括PCB也会以父进程的PCB为模板来初始化。

因为子进程继承了父进程的代码,所以它会打印俩次。创建一个子进程去做父进程一样的事有意义嘛?没有,所以fork()有返回值,通过返回值来区别子进程和父进程。

  • 返回值<0,子进程创建失败
  • 返回值=0,这是子进程
  • 返回值>0,并且等于子进程的PID,这是父进程

所以利用返回值,我们来让子进程做点不一样的事:

  1 #include <stdio.h>
  2 #include <sys/types.h>                                                    
  3 #include <unistd.h>
  4 int main()
  5 {
  6     pid_t i= fork();
  7     if(i==0)
  8     {
  9       printf("i am child\n",i);
 10     }
 11    else if(i>0)
 12    {
 13      printf("i am father\n",i);
 14    }
 15    else 
 16    {
 17      printf("nonono\n");                                                                                                                                                               
 18    }                                                                                                                       
 19                                                                                                             
 20  return 0;                                                                                                                 
 21 }                       

在这里插入图片描述
有两个返回值,是因为有两个进程。

2.4 消灭指定进程

这个用的是指令kill,我们可以看一下kill的选项。
在这里插入图片描述
着重记住:

-9 干掉指定的进程 kill -9 PID
-19 暂停指定进程 kill -19 PID
-18 解除暂停 kill -18 PID

3. 进程的状态

在这里插入图片描述
在读课本的时候,或是听老师讲解时,多数会给出以上的图片。但是具体有哪些状态呢?就三种状态吗?如何进行查看进程状态呢?这都是没解决的问题。

3.1 基本的进程状态
  • R运行状态(running): 并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列
  • S睡眠状态(sleeping): 意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡眠)
  • D磁盘休眠状态(Disk sleep)有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个状态的进程通常会等待IO的结束。
  • T停止状态(stopped): 可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可
    以通过发送 SIGCONT 信号让进程继续运行。
  • X死亡状态(dead):这个状态只是一个返回状态,你不会在任务列表里看到这个状态。
(1)R运行状态。

前文讲过进程是靠进程控制块来管理的,操作系统通过管理进程控制块来决定执行哪个进程。该先执行哪个进程?得排队对吧。所以用队列这种数据结构来管理一堆得进程控制块,轮到谁就运行谁。
在这里插入图片描述
这些进程都是R状态:随时都准备被运行的状态(在运行队列中)。
这个状态给人的感觉就是箭在弦上,蓄势待发,或者已经出发。
验证

一个死循环程序,来看看进程的运行状态,程序名为ll。

#include<stdio.h>
  2 int main()
  3 {                                                                                                                                                                                      
  4   while(1)
  5     ;
  6   return 0;
  7 }
  8 
ps aux|grep 程序名

在这里插入图片描述
可以看到运行状态时R+,这个加号代表在前台运行,如果想要从前台切换到后台运行,那么可以在运行程序命令行后加上&
比如:

./ll &

在这里插入图片描述
可以看到这次为R运行状态。
在这里插入图片描述

(2)S和D睡眠状态

一个是可被打断睡眠,一个是不可被打断睡眠。为什么会有这俩种状态呢?我们进程有可能没有达到可执行的条件。

比如:一个进程需要调用显示器,但是显示器已经被其他的进程给占用了;那么这个进程就需要等待,而且有等待队列,它就去里面排队了。如果它在运行队列中,那么操作系统毫无疑问会把这个进程踢到等待队列里,不让它占用资源。

操作系统可以把S状态的等待进程,踢到等待队列里。但是D状态不行,D状态为不可打断睡眠,操作系统不可以将D状态的进程,放到等待队列中。D状态是大哥呀,睡得死死得,一直得等达到可执行条件满足,而且依旧占用CPU资源。如果想干掉D状态进程,可以考虑关机。

那么为什么存在D状态进程,首先它肯定是有必须等待的理由,比如这个进程正准备在从磁盘里读重要数据,但是磁盘在忙,如果操作系统看见此进程在等待,把它给踢走了,等磁盘将数据拿出来了,该给谁?这个锅谁背?其次,可能就是自己代码写的有些问题,需要去好好查看一下。

等待队列是一个双向链表。
在这里插入图片描述
这里解释两个定义:

  • 将进程从运行队列,搞到等待队列,这就叫做挂起进程。
  • 将进程从等待队列,搞到运行队列,这就叫做唤醒进程。

验证:
还是改改那个上面那个ll程序。

 1 #include<stdio.h>
  2 int main()
  3 {
  4   while(1)
  5   {
  6     printf("hollow S\n");                                                                                                                                                              
  7   }                                                                                                                         
  8   return 0;                                                                                                                 
  9 }                                                                                                                                               

可能有疑问,这不死循环吗?是R状态吧?
在这里插入图片描述
这个程序的状态是S+,+号表示前台运行。是S的原因:IO流太慢了相对于CPU,所以一直是挂起又唤醒,挂起又唤醒的。咱们也许看到刷屏挺快,但对于CPU来说,要输入到显示器上太慢了,所以秒挂起进程,又秒唤醒进程。
至于D状态我就不验证了。

(3)T暂停状态

暂停状态,就是进程暂停不动了。可以用kill -19来暂停进程。
验证
在这里插入图片描述
这就把进程暂停了,
解除暂停kill -18
在这里插入图片描述

(4)X状态进程结束

进程结束,操作系统会回收进程的资源:内核数据结构+代码,数据
有一个疑问:子进程结束就直接结束吗?它是如何结束的?异常退出?正常执行结束?如果是异常结束,证明有问题,需要查出问题所在。所以子进程结束时,父进程会读取进程退出信息。

在这里引出两种特殊的进程状态。

3.2 特殊的进程状态
(1)Z状态僵尸进程

子进程结束,但是它的退出信息没有被父进程读取,子进程会进入Z状态等待被父进程读取退出信息。

验证:可以写个程序

  1 #include<stdio.h>
  2 #include<unistd.h>
  3 int main()
  4 {
  5   pid_t i=fork();
  6   if(i==0)
  7   {
  8     printf("i am child my father:%d\n",getppid());
  9     sleep(5);
 10   }
 11   else 
 12   {
 13     printf("i am father,i am runing\n");
 14     sleep(100);                                                                        
 15   }                                                            
 16   return 0;                                                    
 17 }                                                              
 18                                        

可以看到我让父进程多运行了一段时间,保证子进程退出时,父进程还在运行。
在这里插入图片描述

在这里插入图片描述
子进程的父进程PID是22875,它明显还在运行,但是子进程已经退出,父进程在运行无法查看子进程的退出信息,所以子进程为僵尸进程,状态为Z。

(2)孤儿进程

因为父进程没有读取子进程的退出信息,子进程为Z状态,父进程还在运行没有时间去查看子进程的退出信息,这时候子进程为僵尸进程。
如果父进程比子进程要早结束,这种情况下,因为没有父进程所以被称为孤儿进程。但是孤儿进程会被领养,那就是PID为1的Init,最终也会由Init来读取其退出信息。被领养时,孤儿进程还在运行那么进程状态为R或S;若不运行了,那么它的进程状态为Z,Init接受退出信息后,由Z转为X。

验证:只需要对上面的程序稍作改动,让父进程提前结束掉即可

  1 #include<stdio.h>
  2 #include<unistd.h>
  3 #include<stdlib.h>                                                                     
  4 int main()                           
  5 {                                    
  6   pid_t i=fork();                    
  7   if(i==0)                           
  8   {                                               
  9     printf("i am child my father:%d\n",getppid());
 10     sleep(100);                      
 11   }                                  
 12   else                               
 13   {                                                
 14     printf("i am father,i don't need you,chile\n");
 15     sleep(3);                        
 16     exit(0);                                      
 17   }                                               
 18   return 0;                                       
 19 }                                                 
 20                                    

为了查看最后它的养父Init,用ps-elf|grep ll来查看。这个可以看到父进程的PID。
在这里插入图片描述
首先,父进程没退出时,父进程的PID为28501
在这里插入图片描述
这幅图片,上面的是父进程28501,下面是子进程28502。然后父进程提早结束,看看父进程会是PID为1的Init吗?
在这里插入图片描述
很明显,子进程28502的父进程变成了PID为1的Init。


4.进程的优先级

上文讲过,有运行队列,也就是说在运行队列中的进程都是R状态。但运行队列应该怎样排队呢?
讲一个生活中的例子:假如去做车保养,普通车主可能就得排队,但是VIP车主就不用排队,直接去享受服务。
运行队列也是如此,用进程的优先级进行排队。可不是先来先得,是按照优先级来排列的,假如刚来了一个进程,它的优先级高,那它肯定排在优先级低的前面,可不管你先来后来。

4.1 查看进程的优先级

用这个命令去查看:

ps -l

在这里插入图片描述
-PRI :进程的优先级,数值越小,优先级越大,PRI默认为80
-NI :nice值,进程优先级的修正值,PRI(new)=PRI(默认)+NI

nice值是为了修正PRI值,它的范围是-19~20,也就是40个nice值。nice值为负数,进程的优先级会提高;如果是正数,进程的优先级会变低。这nice范围也不大,这为了防止“饥饿进程”的出现,如果把一个进程的优先级调的非常低,导致进程一直都无法运行,也就是饥饿进程,必须保证每个进程都有口饭吃。

4.2 修改进程优先级

用top命令可以修改进程优先级。

进入top后,
输入r,
再输入进程的PID
最后输入nice值
按q退出 

验证
死循环,方便验证。注意验证要用ps -al指令查看。

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
int main()
{
 while(1)
 {
   printf("hollow PRI\n");
 }
 return 0;
}

在这里插入图片描述
(1)进入top
在这里插入图片描述

(2)输入r,再输入PID,再输入nice值.(每次输入后按回车,这是常识)

大家可以用翻译一下top里的这句话,一般能看懂。
在这里插入图片描述
很明显我输入的nice值是10,所以qq这个进程的PRI变成了90。
在这里插入图片描述
注意:nice值输入100,-1000,这种超出范围的值,也不会报错之类的,默认会修改成nice值的最大(20),或者最小(-19)。


结尾语: 这就是进程的基本概念了,后续还会更新进程的知识。喜欢的朋友,可以点点关注。