您好,欢迎来到百家汽车网。
搜索
您的当前位置:首页【操作系统】03.进程的概念、属性及其调度

【操作系统】03.进程的概念、属性及其调度

来源:百家汽车网

一、进程的概念与理解

概念: 进程是程序的一个执行实例,即正在执行的程序。

上面这句话是大部分的书对进程的解释,但是我们应该如何理解呢?
我们编写代码运行后会在磁盘中会形成一个可执行程序,当我们运行这个可执行程序时,这个程序此时就会被操作系统的调度器加载到内存中;我们上一篇博客中也对操作系统有了认识:操作系统要对进程进行管理,进程的管理包含了加载、调度、切换、释放……而我们可能同时启动多个进程(一面听歌一面看PPT),那么操作系统是如何管理进程的呢?很简单,先描述,后组织,即我们的操作系统对进程的属性进行管理,创建了内核级的数据结构PCB(Linux中的PCB是task_struct)来管理进程。因此实际上进程=内核数据结构(task_struct)+代码和数据。接下来这些task_struct彼此相连构成了struct task_struct* list,而后操作系统就可以对多个进程基于时间片进行轮转调度、切换等等。在这一过程中呈现了动态的特征,因此我们有了如上的结论:进程是运行起来的程序。

下面是来自Linux0.11版本的源码中对task_struct的描述:

struct task_struct {
/* these are hardcoded - don't touch */
	long state;	/* -1 unrunnable, 0 runnable, >0 stopped */
	long counter;
	long priority;
	long signal;
	struct sigaction sigaction[32];
	long blocked;	/* bitmap of masked signals */
/* various fields */
	int exit_code;
	unsigned long start_code,end_code,end_data,brk,start_stack;
	long pid,father,pgrp,session,leader;
	unsigned short uid,euid,suid;
	unsigned short gid,egid,sgid;
	long alarm;
	long utime,stime,cutime,cstime,start_time;
	unsigned short used_math;
/* file system info */
	int tty;		/* -1 if no tty, so it must be signed */
	unsigned short umask;
	struct m_inode * pwd;
	struct m_inode * root;
	struct m_inode * executable;
	unsigned long close_on_exec;
	struct file * filp[NR_OPEN];
/* ldt for this task 0 - zero 1 - cs 2 - ds&ss */
	struct desc_struct ldt[3];
/* tss for this task */
	struct tss_struct tss;
};

二、进程的属性

上面说过PCB相当于是进程属性的集合,Linux中的PCB称作task_struct,那么task_struct中有哪些属性呢?

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

查看进程的指令:ps axj

2.1 PID

PID是用以区分进程唯一性的编号,我们可以使用系统接口getpid来获取当前进程的pid。

#include<stdio.h>     
#include<sys/types.h> 
#include<unistd.h>      
int main()      
{      
    while(1)      
    {      
        printf("I am a process,My Pid is %d\n",getpid());      
        sleep(1);      
    }  
    return 0;  
}     

仅仅通过ps axj显示的进程信息是有些少的,进程的信息被保存在/proc中,以自己的PID为文件名,因此我们也可以通过查看/proc/PID来查看进程的信息

[caryon@VM-24-10-centos ~]$ ll /proc/9139
total 0
dr-xr-xr-x 2 caryon caryon 0 Oct  9 15:47 attr
-rw-r--r-- 1 caryon caryon 0 Oct  9 15:47 autogroup
-r-------- 1 caryon caryon 0 Oct  9 15:47 auxv
-r--r--r-- 1 caryon caryon 0 Oct  9 15:47 cgroup
--w------- 1 caryon caryon 0 Oct  9 15:47 clear_refs
-r--r--r-- 1 caryon caryon 0 Oct  9 15:47 cmdline
-rw-r--r-- 1 caryon caryon 0 Oct  9 15:47 comm
-rw-r--r-- 1 caryon caryon 0 Oct  9 15:47 coredump_filter
-r--r--r-- 1 caryon caryon 0 Oct  9 15:47 cpuset
lrwxrwxrwx 1 caryon caryon 0 Oct  9 15:47 cwd -> /home/caryon/linux/lesson11
-r-------- 1 caryon caryon 0 Oct  9 15:47 environ
lrwxrwxrwx 1 caryon caryon 0 Oct  9 15:47 exe -> /home/caryon/linux/lesson11/code
dr-x------ 2 caryon caryon 0 Oct  9 15:47 fd
dr-x------ 2 caryon caryon 0 Oct  9 15:47 fdinfo
-rw-r--r-- 1 caryon caryon 0 Oct  9 15:47 gid_map
-r-------- 1 caryon caryon 0 Oct  9 15:47 io
-r--r--r-- 1 caryon caryon 0 Oct  9 15:47 limits
-rw-r--r-- 1 caryon caryon 0 Oct  9 15:47 loginuid
dr-x------ 2 caryon caryon 0 Oct  9 15:47 map_files
-r--r--r-- 1 caryon caryon 0 Oct  9 15:47 maps
-rw------- 1 caryon caryon 0 Oct  9 15:47 mem
-r--r--r-- 1 caryon caryon 0 Oct  9 15:47 mountinfo
-r--r--r-- 1 caryon caryon 0 Oct  9 15:47 mounts
-r-------- 1 caryon caryon 0 Oct  9 15:47 mountstats
dr-xr-xr-x 5 caryon caryon 0 Oct  9 15:47 net
dr-x--x--x 2 caryon caryon 0 Oct  9 15:47 ns
-r--r--r-- 1 caryon caryon 0 Oct  9 15:47 numa_maps
-rw-r--r-- 1 caryon caryon 0 Oct  9 15:47 oom_adj
-r--r--r-- 1 caryon caryon 0 Oct  9 15:47 oom_score
-rw-r--r-- 1 caryon caryon 0 Oct  9 15:47 oom_score_adj
-r--r--r-- 1 caryon caryon 0 Oct  9 15:47 pagemap
-r-------- 1 caryon caryon 0 Oct  9 15:47 patch_state
-r--r--r-- 1 caryon caryon 0 Oct  9 15:47 personality
-rw-r--r-- 1 caryon caryon 0 Oct  9 15:47 projid_map
lrwxrwxrwx 1 caryon caryon 0 Oct  9 15:47 root -> /
-rw-r--r-- 1 caryon caryon 0 Oct  9 15:47 sched
-r--r--r-- 1 caryon caryon 0 Oct  9 15:47 schedstat
-r--r--r-- 1 caryon caryon 0 Oct  9 15:47 sessionid
-rw-r--r-- 1 caryon caryon 0 Oct  9 15:47 setgroups
-r--r--r-- 1 caryon caryon 0 Oct  9 15:47 smaps
-r--r--r-- 1 caryon caryon 0 Oct  9 15:47 stack
-r--r--r-- 1 caryon caryon 0 Oct  9 15:47 stat
-r--r--r-- 1 caryon caryon 0 Oct  9 15:47 statm
-r--r--r-- 1 caryon caryon 0 Oct  9 15:47 status
-r--r--r-- 1 caryon caryon 0 Oct  9 15:47 syscall
dr-xr-xr-x 3 caryon caryon 0 Oct  9 15:47 task
-r--r--r-- 1 caryon caryon 0 Oct  9 15:47 timers
-rw-r--r-- 1 caryon caryon 0 Oct  9 15:47 uid_map
-r--r--r-- 1 caryon caryon 0 Oct  9 15:47 wchan

上述显示中本篇博客重点研究的是cwd和exe

/proc是系统为我们提供的一个访问进程信息的接口,因此实际上的ps就是对/proc进行相关的文本分析,而/proc也并不是磁盘级文件,因此频繁的创建于删除并不影响效率。

2.2 PPID

在Linux系统中,启动之后,新建的任何进程都是由自己的父进程创建的,我们可以使用系统调用接口getppid()获取当前进程的ppid。

#include<stdio.h>    
#include<unistd.h>    
int main()    
{    
    while(1)    
    {    
        printf("PID:%d,PPID:%d\n",getpid(),getppid());                                
        sleep(1);    
    }    
    return 0;    
}    

[caryon@VM-24-10-centos ~]$ ps ajx|head -1;ps ajx|grep 31222
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
31222 28570 28570 31222 pts/0    28570 S+    1001   0:00 ./code
31220 31222 31222 31222 pts/0    28570 Ss    1001   0:00 -bash
 7482 31749 31748  7482 pts/1    31748 S+    1001   0:00 grep --color=auto 31222

原来是bash(Linux下的命令行解释器)。命令行中,执行命令/程序本质上就是bash创建子进程,由子进程执行我们的代码

那么子进程是如何创建的呢?
系统接口提供了fork()来创建子进程,如果创建成功则给父进程返回子进程的PID,给子进程返回0,失败则给父进程返回-1,子进程创建失败。

#include<stdio.h>
#include<unistd.h>

int gal=0;

int main()
{
    printf("我的PID是%d,我的PPID是%d\n",getpid(),getppid());
    pid_t id=fork();
    if(id==0)
    {
        while(1)
        {
            printf("我是子进程,我的PID是%d,我的PPID是%d,gal=%d\n",getpid(),getppid(),gal);
            sleep(1);
            gal++;
        }
    }

    else
    {
        while(1)
        {
            printf("我是父进程,我的PID是%d,我的PPID是%d,gal=%d\n",getpid(),getppid(),gal);
            sleep(1);
        }
    }
    return 0;
}
[caryon@VM-24-10-centos lesson11]$ ./code
我的PID是13111,我的PPID是31222
我是父进程,我的PID是13111,我的PPID是31222,gal=0
我是子进程,我的PID是13112,我的PPID是13111,gal=0
我是父进程,我的PID是13111,我的PPID是31222,gal=0
我是子进程,我的PID是13112,我的PPID是13111,gal=1
我是父进程,我的PID是13111,我的PPID是31222,gal=0
我是子进程,我的PID是13112,我的PPID是13111,gal=2
我是父进程,我的PID是13111,我的PPID是31222,gal=0
我是子进程,我的PID是13112,我的PPID是13111,gal=3
我是父进程,我的PID是13111,我的PPID是31222,gal=0
我是子进程,我的PID是13112,我的PPID是13111,gal=4

fork()函数一旦调用后面就会有两个进程了,这两个进程各自执行各自的代码,只不过fork()给父进程返回的是子进程的PID,给子进程则返回0,也就是说fork()函数有两个返回值

#include<stdio.h>
#include<unistd.h>

int gal=0;
int main()
{
    pid_t id=fork();
    if(id==0)
    {
        while(1)
        {
        printf("I am a subprocess,my pid is %d,my ppid is %d,gal = %d\n",getpid(),getppid(),gal++);
        sleep(1);
        }
    }
    else
    {
        while(1)
        {
        printf("I am a parentprocess,my pid is %d,my ppid is %d,gal = %d\n",getpid(),getppid(),gal);
        sleep(1);
        }
    }
    return 0;
}

在fork()函数中return语句之前其实就已经创建好子进程了,而返回的id也是一个变量,返回变量实际上就是向指定变量写入数据,所有两个进程拿到id各自运行,id也是私有的数据。

fork()之后谁先执行?由操作系统自主决定。

2.3 状态

2.3.1 进程的状态

  • 并行与并发
  • 时间片
    LInux/windows这些民用级别的操作系统都是分时操作系统,根据时间片进行调度轮转的,与之相对的是实时操作系统
  • 进程具有性
  • 认识运行、阻塞与挂起
    当把程序加载到内存中之后,进程就会被链接到运行队列中,而后对队列进行管理,当程序运行到需要访问外设时,进程就会被链入到阻塞队列中
    因此运行状态就是进程在运行队列中,阻塞状态就是进程在阻塞队列中,两者的本质区别就是让进程在不同队列中。而当内存资源严重不足时,阻塞队列中的进程在等待外设响应,操作系统就会将这些进程中的数据换出到磁盘上,当外设操作完后再被换入进程,磁盘上对此有专门的swap分区专门用于换入换出操作,而这一操作对应的进程就是挂起状态。挂起状态本质是以时间换空间的做法。更严重时系统甚至会直接终止掉进程。

2.3.2 Linux下的进程状态

下面这段代码是Linux0.11版本中的状态源码:

/*
* 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)", /* 0 */
"S (sleeping)", /* 1 */
"D (disk sleep)", /* 2 */
"T (stopped)", /* 4 */
"t (tracing stop)", /* 8 */
"X (dead)", /* 16 */
"Z (zombie)", /* 32 */
};

R :运行状态
S :浅度休眠状态,可以被kill
D:磁盘休眠状态,不可被kill

D状态存在的意义在于,当内存资源严重不足时,操作系统可能会将正在向磁盘传输数据的进程杀掉以维持自身安全,但是数据是不可恢复的,一旦进程被终止,我们并不知道传输数据这个操作是否成功,因此引入了D状态,使得操作系统不能杀死处于D状态的程序。

T:暂停状态,通常是进程做了非法但不致命的操作,只能用kill -9终止
t :追踪暂停状态,常见调试时打断点
X:死亡状态
Z:僵尸状态,用于维护自己的task_struct,方便为了父进程读取进程退出信息

谈论X状态和Z状态,我们需要从进程为何被创建说起。进程创建的目的自然是为了完成用户的任务。那么我们如何判断任务的完成结果呢?
进程=内核数据结构(task_struct)+代码和数据 ,这是上面我们给出的进程定义。
当进程退出时,首先被释放的就是进程对应的程序信息,然后把退出信息保存在自己的task_struct中,我们的操作系统必须要维护着这个已经退出的进程,方便用户可以获取退出信息,因此我们的操作系统中就有了Z状态。我们可以使用$?来查看最近的一个进程的退出信息。
但是还有一点需要注意:当我们使用kill -9 杀掉子进程后,子进程就会变成Z状态,如果不对这个僵尸进程进行处理的话,就会产生内存泄漏。因此一般情况下都需要父进程读取了子进程信息后子进程才会自动退出。

2.3.3 孤儿进程

上面我们知道了当子进程退出,父进程没有退出时,子进程就是僵尸进程;那么当父进程退出,子进程没有退出时是什么情况呢?
这就是孤儿进程,子进程会被系统领养,当它退出时,系统会对这个子进程进行处理回收。

2.4 优先级

我们可以使用ps -l 查到优先级信息:

优先级本质上就是对某种资源获取的先后顺序,这种资源往往是稀缺的。在进程层次来看,优先级竞争的是CPU资源。task_struct中有优先级属性,它是通过几个int类型的变量来表示优先级的。上图中的PRI和NI两个属性影响进程的优先级,其中PRI是默认优先级(80),NI是优先级的修正数据([-20,20))

优先级如何进行调整呢?
使用top指令,输入r,然后根据提示进行调整。

但是一般情况下我们不对优先级进行调整,即使调整也要保证nice值有一定的范围,这是因为我们的操作系统是分时操作系统,对进程的调度要尽量公平。

2.5 UID

2.4的图中我们还看到了UID这一属性,UID全称User Identify,是用以标记进程是谁启动的。
在文件显示时,我们可以使用ll -n来以数字显示文件的相关属性,这个数字也是UID。

[caryon@VM-24-10-centos linux]$ ll -n
total 2452
drwxrwxr-x 2 1001 1001   4096 Sep 16 11:39 lesson10
drwxrwxr-x 2 1001 1001   4096 Oct 10 20:02 lesson11
drwxrwxr-x 2 1001 1001   4096 Oct 12 11:20 lesson12
drwxrwxr-x 2 1001 1001   4096 Oct 12 11:24 lesson13
drwxrwxr-x 2 1001 1001   4096 Jul 12 16:50 lesson2
drwxrwxr-x 2 1001 1001   4096 Jul 17 11:28 lesson3
drwxrwxr-x 2 1001 1001   4096 Aug 26 17:38 lesson4
drwxrwxr-x 2 1001 1001   4096 Aug 27 19:12 lesson5
drwxrwxr-x 2 1001 1001   4096 Aug 28 19:38 lesson6
drwxrwxr-x 2 1001 1001   4096 Sep 15 18:26 lesson7
drwxrwxr-x 2 1001 1001   4096 Sep  2 13:55 lesson8
drwxrwxr-x 2 1001 1001   4096 Sep 16 10:17 lesson9
-rw-rw-r-- 1 1001 1001 6045 Sep 16 13:28 工具.png
-rw-rw-r-- 1 1001 1001 926836 Sep 16 13:28 指令.png
-rw-rw-r-- 1 1001 1001 9245 Sep 16 13:28 权限.png

前面我们知道文件有自己的权限,即拥有者、所属组;我们还知道Linux下一切皆文件,所有的操作都是进程操作,因此进程会记录是谁启动的这个进程。通过UID与文件的拥有者、所属组进行对比实现了对权限的控制。

2.6 通过进程切换理解上下文

Linux下的进程是基于时间片进行调度轮转的,这也就是意味着时间片到了进程就要切换,但是一个进程在时间片到了的时候并不一定跑完了,处在任何状态都可能被切换。那么如何进行进程切换呢?

进程在运行的时候会有很多的临时数据(这些临时数据我们称之为上下文数据)被保存在CPU寄存器中(我们这里这里只关注eip(pc)和ir):

  • pc: 存储当前正在执行指令的下一条指令的地址
  • ir: 记录的就是当前正在执行的指令

CPU内部的寄存器数据是进程执行时的瞬时数据。进程在进行切换的时候就需要先保存切换时刻pc和ir寄存器中的数据,这样才能在时间片轮转到这个进程时进行数据的恢复。

因此进程切换的核心就是进程上下文数据的保存与恢复
CPU内部有很多的寄存器,但是统称为一套寄存器(注意),与之相对的是每个进程都有自己的上下文数据,被保存在CPU寄存器内,而寄存器只有一套,这说明寄存器是被多个进程共享的。

三、进程调度

在课本上给出的进程调度算法多数情况下都是采用队列的FIFO调度算法,但是这种情况面对优先级时非常的棘手,因此真实的Linux调度算法并不是这样的,它采用了哈希桶结构,这可以更好体现出进程的优先级。

接下来将结合Linux2.6内核源码对LInux下进程调度进行分析:

此外我们还要知道在Linux中链接方式的独特之处,他采用了单独的结点域来进行链接,大大的提高了我们的效率,并且也决定了我们的一个进程可以处于不同队列的特性:

因篇幅问题不能全部显示,请点此查看更多更全内容

Copyright © 2019- baijiahaobaidu.com 版权所有 湘ICP备2023023988号-9

违法及侵权请联系:TEL:199 18 7713 E-MAIL:2724546146@qq.com

本站由北京市万商天勤律师事务所王兴未律师提供法律服务