进程的基本理解


进程的基本理解

进程的概念属于Linux系统编程的一部分,Linux系统编程要解决的问题有4点。

  1. 任务与任务之间的关系
  2. 任务与任务之间的同步
  3. 任务与任务之间的通信
  4. 任务与任务之间的作用

冯诺依曼体系结构

虽然进程属于计算机软件的概念,但是理解计算机硬件的基本组成对学习软件也有帮助。计算机硬件的基本结构是冯诺依曼体系结构。该结构认为计算机硬件可以大致被分为:输入设备、输出设备(I/O设备)、内存、CPU.其中CPU的核心是运算器和控制器。

在这里插入图片描述

CPU中的运算器可以进行运算,这里的运算不是常规的1+1=2,这种运算被称为算数运算,运算器不仅可以进行算数运算,也可以进行逻辑运算,判断真假也属于运算。这两种运算简称算罗运算。CPU中的控制器可以响应外部事件,起到控制作用。

内存

在冯诺依曼体系结构中有内存这个东西,I/O设备与CPU之间的交互都是通过内存完成的。原因:在冯诺依曼体系结构中,输入设备的本质是产生数据,输出设备的本质是保存数据,输入设备不能直接把数据交给CPU处理,输出设备也不能直接从CPU中拿数据,这个过程要通过内存间接完成,本质上是各种硬件的运算速度不同,CPU的运算速度要远远大于I/O设备,于是通过把数据转到内存,CPU直接从内存中拿数据,可以缩小因为硬件的运算速度差距造成的影响。冯诺依曼体系结构在输入设备、输出设备和CPU之间加了一个内存,提高了外设与CPU之间的交互速度,外设把数据加载到内存,CPU从内存中读取数据,经过运算,写回内存,数据再从内存加载到外设,大大提高了外设与CPU交互的速度。

离CPU越近的存储设备,速度越快,价格越高,越远的则相反。速度:CPU中的寄存器>内存>硬盘。CPU中的寄存器和内存属于掉电易失的存储介质,只要断电,里面的内容就容易丢失,硬盘则能长时间存储数据。

对于一个程序,想要运行它,必须要先加载到内存,这是由冯诺依曼体系结构决定的,因为CPU只和内存打交道。

操作系统的基本理解

进程管理是操作系统的核心之一,要理解进程,首先要对操作系统有一个大概的了解。

操作系统是一个对软硬件资源进行管理的软件。对上操作系统需要给用户提供一个良好的使用环境,对下操作系统要管理好软硬件资源,保证系统的稳定性。对软硬件资源的管理是手段,对上提供一个良好的使用环境是目的。

操作系统的管理

采用类比的方式理解操作系统的管理。

我们将学校中的人分为3种,校长,辅导员,学生。其中校长是管理者,学生是被管理者。管理者与被管理者之间可以不直接沟通,管理者只要能够拿到被管理者的数据,通过分析被管理者的数据进行某种决策,让辅导员执行该决策,就间接实现了管理的目的。校长对于学生的管理,不是对学生本身的管理,是对学生数据的管理。管理,是对被管理对象的数据进行管理

两个基本的问题:Q1:校长如何拿到学生的数据?Q2:校长的决策由谁执行?

校长通过辅导员拿到学生的数据,校长的决策由辅导员执行。辅导员是执行者。校长是操作系统,学生是底层的硬件,而辅导员则充当驱动程序的角色。操作系统是对软硬件进行管理的程序,校长管理辅导员(驱动程序,软件),也管理学生(硬件)。校长管理学生是进行先描述,在组织。校长可以将每一名学生定义为一个结构体对象。再把每一个结构体对象通过链表的形式链接在一起,这样对于学生的管理就变为了对链表的增删查改。

Linux内核是用C语言写的,Linux操作系统对硬件的管理本质上是对硬件的所有属性数据做管理。操作系统拿到硬件的数据,将其抽象为统一的数据结构,这样一来,对硬件的管理就变为对该数据结构的操作。

Linux操作系统的核心:内存管理,进程管理,驱动管理,文件系统。操作系统是假设所用用户都不值得信任的,用户不能直接访问操作系统,一是直接访问的方式成本太高,二是不安全。因此,操作系统只对外提供系统接口,我们可以调用这些接口,称为system call,系统调用。这些系统接口本质上就是用C语言写的函数。调用这些接口就是在调用函数。但是由于系统调用的接口使用起来成本也比较高,而且有些接口还和操作系统本身有关,所以在系统接口之上提供了图形化界面和shell外壳程序。相当于是对系统调用接口的封装,这些封装又被称为第三方库,把系统调用接口以第三方库的形式封装起来供使用,大大降低了使用难度。

进程

在Linux操作系统下,运行一条命令(ls,pwd),或者./a.out运行一个程序,在系统层面,就是创建了一个进程,当一个程序被加载到内存中时,严格来说已经不能被称为程序,应该被叫做一个进程。Linux同时存在大量的进程在系统中,需要对这些进程进行管理,如何管理?答案是先描述,在组织。

描述

当同时存在大量的进程在系统中时,Linux操作系统为了描述不同的进程,给每一个进程创建了他所对应的PCB结构体,全称是进程控制块。这个结构体里面记录了进程的所有属性。将每一个进程的PCB结构体用链表链接起来,这样对于进程的管理就变为对PCB结构体链表的增删查改,也就变成了对于一种具体数据结构的操作。

进程=对应的代码和数据+描述该代码和数据的PCB结构体,单纯的代码和数据严格来说不能被称为进程

PCB

PCB的全称是process control block,进程控制块。PCB是进程控制块的统称,在不同的操作系统下,进程控制块是不一样的,Linux系统下的PCB结构体是struct task_struct.

task_struct是Linux内核的一种数据结构,它会被装载到内存中并且里面包含进程的所有属性。

struct task_struct
{
    标识符(pid,一个无符号整数):描述该进程的唯一符号,用来区分其他进程;
    状态:任务状态,退出代码,退出信号;
    优先级:该进程相对于其他进程的优先级;
    程序计数器:程序中即将被执行的指令的下一条指令的地址;
    内存指针:简单理解就是指向程序对应的代码和数据在内存中位置的指针;
    上下文数据:进程执行时处理器的寄存器中的数据;
    I/O状态信息;
    记账信息:该进程被处理器所处理的总时间,等等;
    ......................................
};

组织进程

所有运行在Linux操作系统的中进程,他们的属性都是以task_struct链表的形式存在内核里面的,每个进程都有它的task_struct,每一个进程的task_struct最终都会以链表的形式链接到一起,通过对该链表的操作,实现对进程的管理。这个链表不是普通的单链表,是一种特殊的双向链表

查看进程

查看进程使用ps命令。

[slowstep@localhost ~]$ ps
   PID TTY          TIME CMD
  2365 pts/0    00:00:00 bash
  3473 pts/0    00:00:00 ps
//PID表示该进程的进程ID,bash这个进程的进程id是2365.
//bash是linux的shell外壳程序

运行ps,本质上也是创建进程,所以会有ps的进程id

ps常用的搭配

ps -ajx
ps -ajx | head -1 && ps -ajx | grep '关键字'
//&&表示左边的命令执行成功就指向右边的,否则就不执行右边的

Linux下所有的进程都在/proc这个目录下。

ls -al /proc

可以在该目录下找到当前的进程信息

ls -al /proc | grep '进程的id号'

获取当前进程的pid与ppid

NAME
       getpid, getppid - get process identification

SYNOPSIS
       #include <sys/types.h>
       #include <unistd.h>

       pid_t getpid(void);
       pid_t getppid(void);
//pid_t 是无符号整数,相当于unsigned int

使用的函数是getpid和getppid,需要包含<sys/types.h>和<unistd.h>头文件。ppid是当前进程父进程的进程id

#include<stdio.h>
#include<sys/types.h>
#include<unistd.h>
int main()
{
    pid_t id=getpid();
    while(1)
    {
        printf("----------------\n");
        printf("当前进程的id是%d\n",id);
        printf("当前进程的父进程id是%d\n",getppid());
        printf("----------------\n");
        sleep(2);
    }
    return 0;
}

结果:

当前进程的id是4141
当前进程的父进程id是2365
ps -ajx | head -1 && ps -ajx | grep '1.out'
PPID    PID   PGID    SID TTY       TPGID STAT   UID   TIME COMMAND
2365   4141   4141   2365 pts/0      4141 S+    1000   0:00 ./1.out
4087   4187   4186   4087 pts/1      4186 R+    1000   0:00 grep --color=auto 1.out

可以看到pid为4141的进程的父进程id是2365

ls -al /proc | grep '2365' 
dr-xr-xr-x.   9 slowstep       slowstep                     0 Aug 20 20:54 2365
ls -al /proc | grep '4141' 
dr-xr-xr-x.   9 slowstep       slowstep                     0 Aug 20 22:28 4141
ps -ajx | head -1 && ps -ajx | grep '2365'
PPID    PID   PGID    SID TTY       TPGID STAT   UID   TIME COMMAND
2357   2365   2365   2365 pts/0      4141 Ss    1000   0:00 -bash

pid为2365的进程的ppid是2357,这个进程是Linux的shell外壳程序bash.我们平时运行的命令或者可执行程序都是以bash的子进程的方式去运行的

杀掉进程(kill)

kill -9 4141
----------------
当前进程的id是4141
当前进程的父进程id是2365
----------------
Killed
kill -l      //可以查看所有可用的信号

创建子进程

创建子进程使用fork函数

NAME
       fork - create a child process

SYNOPSIS
       #include <unistd.h>

       pid_t fork(void);

fork函数有两个返回值。

  1. fork函数创建子进程成功,给父进程返回子进程的pid,给子进程返回0
  2. 创建失败,返回-1
#include<stdio.h>
#include<sys/types.h>
#include<unistd.h>
int main()
{
    printf("我是父进程,我的pid是%d\n",getpid());
    pid_t id=fork();
    if(id==-1)
    {
        perror("创建子进程失败:");
    }
    else if(id>0)
    {
        printf("创建子进程成功!我是父进程,我的pid是%d,我的ppid是%d\n",getpid(),getppid());
    }
    else if(id==0)
    {
        printf("我是子进程,我的pid是%d,我的ppid是%d\n",getpid(),getppid());
    }
    else //理论而言不可能有else
    {
        ;
    }
    return 0;
}

结果

我是父进程,我的pid是4603
创建子进程成功!我是父进程,我的pid是4603,我的ppid是2365
我是子进程,我的pid是4604,我的ppid是1  #这里子进程的ppid=1的原因是子进程变为孤儿进程进行被1号进程领养

在fork函数之后,代码父子共享,实际上fork函数内部可以分为2个阶段,首先创建子进程,然后return id,也就是说,在return id之前,子进程已经创建完毕。return id以及其后的代码都是父子共享的,由于在return id以后父子进程的id值不一样,但是父子进程的id的虚拟地址是一样的,当虚拟地址一样而id值不一样会发生写时拷贝,映射到物理内存的不同区域,所以fork函数有2个返回值,并且父子进程打印出来的id虚拟地址是一样的。fork函数之后,代码父子共享,但是可以使用if/else控制父子能执行的代码,代码共享不代表都能执行.

进程调度

每一个进程都有它对应的task_struct结构体,进程调度相当于是在task_struct结构体形成的队列中挑选一个进程的过程,操作系统和CPU运行一个进程,就是从队列中挑选一个task_struct结构体,来执行它所对应的代码和数据。

动态检测进程

每隔一秒动态检测:

while : ;do ps -ajx | head -1 && ps -ajx | grep a.out | grep -v grep ;sleep 1;done

其中的a.out是可执行文件,执行起来以后对应一个进程。grep -v grep表示不匹配含有’grep’的信息

fork函数为什么这样返回?

fork函数给父进程返回子进程的pid,给子进程返回0,原因在于一个父进程可以有多个子进程,父进程需要管理好子进程,所以需要子进程的pid,而子进程只有一个父进程,父进程:子进程=1:n,给子进程正常返回0即可。

进程状态

对于进程的基本认识:

  1. CPU在执行进程任务时,是根据运行队列(run_queue)的先后顺序来的
  2. 进程调度是系统和CPU运行一个进程的过程
  3. fork()函数可以创建子进程,但是父子进程谁先运行是不知道的,由系统调度器决定,系统调度器可能先运行子进程,在运行父进程
  4. 当服务器压力过大时,OS可能杀掉一些进程,节省空间。

进程有不同的状态:

  • 新建:当一个进程的task_struct没有进入运行队列时,称为新建状态。在Linux内核中没有这种状态,在Linux内核中,一个进程只要产生,就会立刻进入运行队列

  • 运行:该进程的task_struct结构体正在运行队列中排队,进程还没有被执行。运行态,指的是task_struct在运行队列中排队,可能没有真正被运行。一个进程访问CPU要排队,访问磁盘,网卡,显卡等也要排队。访问CPU需要排队,这个队列称为运行队列,访问硬盘、显卡等需要排队,这些队列被称为阻塞队列。进程在排队等待CPU的资源的状态叫做运行态

  • 阻塞:阻塞状态指的是排队等待非CPU资源的就绪。例如scanf函数,在等待输入设备的输入。下载文件时网络卡死,就是在等待网卡就绪。把这种等待非CPU资源就绪的状态叫做阻塞状态。

  • 挂起:如果某一个进程在内存中,它的代码长时间没有被执行(原因可能是CPU需要执行的进程任务太多,暂时管不了这个进程),那么该进程占用了内存空间而且暂时没有被处理。此时,将该进程的代码和数据交换到磁盘的SWAP分区,等到CPU空闲了,有时间执行这个进程了,再把它的数据从SWAP分区转到内存。这种状态叫做挂起状态,即将长时间未被执行的进程的代码和数据交换到SWAP分区的状态。虽然处于挂起状态的进程它的代码和数据交换到了SWAP分区,但是它的task_struct结构体还在内存中,可以对该进程起到管理的作用。

使用ps命令可以查看到进程的不同状态。

PPID    PID   PGID    SID TTY       TPGID STAT   UID   TIME COMMAND
1947   3026   3026   1947 pts/0      3026 S+    1000   0:00 ./a.out

STAT就表示进程的状态。

  • + :加号表示前台进程,前台进程在运行的时候不能执行其他命令。没有加号的是后台进程,后台进程在运行的时候可以执行其他命令。

    ./a.out  //前台进程
    ./a.out &  //后台进程
    
  • S :S表示休眠,对应阻塞状态,即该进程在等待某种非CPU资源的就绪,S表示的休眠状态可以中断,S状态的进程可以通过kill命令终止。也可能出现这样的情况:处于S状态的进程长时间在等待非CPU资源的就绪,而且还在内存中占用空间,如果此时CPU需要执行的进程任务太多,内存空间已经不够用了,那么操作系统会直接干掉这个S状态的进程,为其它进程在内存中腾出空间,即使这个S状态的进程在等待磁盘的写入数据。S状态的进程如果被操作系统干掉的话,可能会造成数据丢失等问题。

  • D :D状态也是睡眠状态,D状态被称为磁盘睡眠,是深度睡眠状态,即D状态的进程也是对应阻塞状态,也是在等待某种非CPU资源的就绪。但是D状态的进程不可被动唤醒,如果一个D状态的进程正在等待向磁盘写入数据,即使此时内存中存在大量需要被CPU执行的进程任务,操作系统也不能把D状态的进程杀掉,包括使用kill -9命令也无法杀掉D状态的进程。只有等到D状态的进程等待磁盘资源完毕,并且写入数据完毕,自动退出。D状态的进程除了自己退出,另外的办法就是关机重启,所以内存中不能有太多D状态的进程。使用dd命令可以演示D状态的进程

  • T(t) :T或者t表示进程处于暂停状态,这个最常见的场景就是使用gdb调试,打断点运行到某一位置停下,此时进程就处于暂停状态。S状态与T状态的区别是S状态的进程是在等待非CPU资源的就绪,T状态的进程是单纯的暂停。S状态是进程在等,T状态是进程暂停了,不动了。

  • X :X状态表示进程终止,dead,可以被立刻回收.X状态一般看不到,因为处于X状态的进程在一瞬间就被系统回收了(连同它的PCB)

  • Z :Z状态表示僵尸状态,指的是虽然进程已经退出,但是操作系统不能立刻回收,因为处于Z状态的进程退出原因比较蹊跷,需要让该进程的父进程或者操作系统检测一下这个进程,到底是因为什么原因退出的,只有当检测完毕之后,Z状态的进程才变为X,被父进程和OS回收。使用fork函数可以模拟出子进程的的Z状态。

当使用fork函数创建一个子进程时,如果子进程先执行完,子进程就会处于Z状态,此时,父进程必须调用wait函数或者waitpid函数来回收处于z状态的子进程。

Z状态进程与孤儿进程

  • Z状态的进程,可以认为它的PCB结构体还在内存中,但是它的代码和数据已经不在内存中了,因为Z状态的进程已经执行完了,只是等待它的父进程或者操作系统回收。如果Z状态的进程一直不被回收,就会造成内存泄漏。回收Z状态的进程,需要由父进程调用wait/waitpid函数。

    #include<sys/types.h> //wait函数头文件
    #include<sys/wait.h>
    pid_t wait(int* status);
    

    当一个进程调用wait函数,这个函数就会自动分析当前进程是否存在某个子进程处于Z状态,如果存在,wait就会收集这个子进程的信息,并且把它彻底销毁后返回,如果没有找到处于Z状态的子进程,那么wait函数会一直阻塞,直到有一个处于Z状态的子进程为止。如果一个进程没有子进程,wait函数就会调用失败,返回-1.只要一个进程有子进程,并且调用了wait函数,那么wait函数就会一直等,直到等到子进程变为Z状态。然后wait函数自动捕获这个处于Z状态的进程,拿到它的pid并且销毁它。

    #include<unistd.h>
    #include<sys/types.h>
    #include<sys/wait.h>
    #include<stdio.h>
    int main()
    {
        pid_t id=fork();
        if(id==-1)
        {
            return 0;
        }
        else if(id==0)
        {
            for(int i=0;i<10;i++)
            {
                printf("I am child process,pid:%d,ppid:%d\n",getpid(),getppid());
                sleep(1);
            }
        }
        else 
        {
            printf("I am father,pid:%d\n",getpid());
            pid_t id=wait(NULL);
            printf("捕获到了一个Z状态的进程,id是%d\n",id);
        }
        return 0;
    }
    
    [slowstep@localhost mydir]$ ./a.out 
    I am father,pid:10530
    I am child process,pid:10531,ppid:10530
    I am child process,pid:10531,ppid:10530
    I am child process,pid:10531,ppid:10530
    I am child process,pid:10531,ppid:10530
    I am child process,pid:10531,ppid:10530
    I am child process,pid:10531,ppid:10530
    I am child process,pid:10531,ppid:10530
    I am child process,pid:10531,ppid:10530
    I am child process,pid:10531,ppid:10530
    I am child process,pid:10531,ppid:10530
    捕获到了一个Z状态的进程,id是10531  //3~12行wait函数一直在等待
    
  • 孤儿进程:孤儿进程指的是一个进程自身还在跑,但是它的父进程已经结束了,这样的话,最后这个进程找不到它的父进程,一旦该进程指向结束,会一直处于Z状态,没有它的父进程来回收它,这种进程叫做孤儿进程。一般孤儿进程会被1号进程领养,1号进程的名字是init,就是操作系统。即孤儿进程会被操作系统领养。

    #include<stdio.h>
    #include<unistd.h>
    #include<sys/types.h>
    int main()
    {
        printf("父进程的pid是:%d\n",getpid());
        pid_t id=fork();
        if(id==-1){
            printf("子进程创建失败!\n");
            return 0;
        }
        else if(id>0){
            int cnt=3;
            while(cnt>=0)
            {
                printf("我是父进程,我的pid是%d\n",getpid());
                cnt--;
                sleep(1);
            }
        }
        else{
            int cnt=5;
            while(cnt>=0)
            {
                printf("我是子进程,我的pid是%d,我的ppid是%d\n",getpid(),getppid());
                cnt--;
                sleep(1);
            }
        }
        return 0;
    }
    

    结果:

    父进程的pid是:16198
    我是父进程,我的pid是16198
    我是子进程,我的pid是16199,我的ppid是16198
    我是父进程,我的pid是16198
    我是子进程,我的pid是16199,我的ppid是16198
    我是子进程,我的pid是16199,我的ppid是16198
    我是父进程,我的pid是16198
    我是子进程,我的pid是16199,我的ppid是16198
    我是父进程,我的pid是16198
    我是子进程,我的pid是16199,我的ppid是16198
    [slowstep@localhost 循环队列]$ 我是子进程,我的pid是16199,我的ppid是1
    

    父进程先执行完,子进程才跑完,当父进程和子进程都在跑的时候,子进程的ppid是16198,一旦父进程跑完,子进程就变为孤儿进程,被1号进程领养。此时,子进程的ppid变为1.

优先级

进程与进程之间存在优先级的概念。由于CPU只有一个,而进程有多个,于是就产生了哪一个进程先享受CPU资源,哪一个进程后享受CPU资源的问题。进程的优先级是由调度器进行评判的。

进程的优先级=老的优先级+nice值,nice值的范围是[-20,19],在计算进程的优先级时,每一次老的优先级都是80.优先级越小,进程就越优先被执行。nice值越小,进程的优先级值越小,但是越先被执行。

查看优先级:

ps -la
ps -la
F S   UID    PID   PPID  C PRI  NI ADDR SZ WCHAN  TTY          TIME CMD
0 S  1000  16435  14758  0  80   0 -  3307 hrtime pts/0    00:00:00 a.out

PRI表示优先级,NI表示nice值

更改nice值。

1.top
2.r
3.输入pid
4.输入nice值

进程的性质

  1. 进程具有独立性,一个进程出现问题基本不会影响另外一个进程。包括父子进程之间也有独立性,父子进程可能代码共享,但是不会相互影响,即父进程没了,子进程该怎么执行怎么执行。只不过此时子进程被1号进程领养了

  2. 进程具有竞争性,即进程与进程之间会竞争CPU资源。

  3. 进程有并发和并行

    并行与并发:并行指的是有多个CPU,多个进程同时在跑。并发指的是只有一个CPU,每个进程有自己的时间片,把自己的时间片跑完了就要从CPU上下来。并发的进程存在抢占与出让的行为,一个优先级高的进程可能抢占一个优先级低的进程,即使这个进程的时间片还没有跑完。一个进程如果运行完了还没有达到自己的时间片,会出让CPU.

    并行的CPU各自也是并发的。

    对于并发的进程,该进程在被CPU执行时,一定要把自己的数据放到CPU中的寄存器中,CPU中的寄存器保存该进程的临时数据。这个数据被称为进程的上下文数据。当一个进程的时间片跑完了,他要带走自己的上下文数据。等到下次在享受CPU资源时,再把上下文数据加载到寄存器中,寄存器只有一个,但是不同的进程有它们各自的上下文数据,上下文数据有多份。进程在运行的时候,寄存器中保存的一定是临时数据,进程的上下文数据一定不能删除,否则等到下次cpu在执行的时候不知道从哪一个位置开始执行代码了。

. 进程具有竞争性,即进程与进程之间会竞争CPU资源。

  1. 进程有并发和并行

    并行与并发:并行指的是有多个CPU,多个进程同时在跑。并发指的是只有一个CPU,每个进程有自己的时间片,把自己的时间片跑完了就要从CPU上下来。并发的进程存在抢占与出让的行为,一个优先级高的进程可能抢占一个优先级低的进程,即使这个进程的时间片还没有跑完。一个进程如果运行完了还没有达到自己的时间片,会出让CPU.

    并行的CPU各自也是并发的。

    对于并发的进程,该进程在被CPU执行时,一定要把自己的数据放到CPU中的寄存器中,CPU中的寄存器保存该进程的临时数据。这个数据被称为进程的上下文数据。当一个进程的时间片跑完了,他要带走自己的上下文数据。等到下次在享受CPU资源时,再把上下文数据加载到寄存器中,寄存器只有一个,但是不同的进程有它们各自的上下文数据,上下文数据有多份。进程在运行的时候,寄存器中保存的一定是临时数据,进程的上下文数据一定不能删除,否则等到下次cpu在执行的时候不知道从哪一个位置开始执行代码了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值