自学内容网 自学内容网

进程的概念

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档

一、进程

进程是什么东西呢 ?
和我们的程序有什么关系和区别呢 ?
请添加图片描述

请添加图片描述

上面的.exe 是不是一个应用程序啊,后面写着,但是当我们双击这个应用程序的时候,这时候这个程序就变成了一个进程,开始运行了起来。

我们来看看程序和进程的定义
程序:程序是一组预先编写好的指令,用于执行特定任务。它是一个静态实体,存储在磁盘上,直到被加载到内存中才能执行。
进程:进程是程序的一次执行实例。当一个程序被加载到内存并开始运行时,它就变成了一个进程。进程是一个动态实体,会随着时间和环境的变化而变化。

这样一来再看定义是不是就清晰了一点。最后在总结一下,进程是程序的实体。程序要运行,首先得加载到内存中,而正在运行的程序就叫做一个进程,操作系统中会同时运行很多个进程 。(由第一个图可以看出来)

二、进程控制块

先说一说操作系统

任何计算机系统都包含一个基本的程序集合,称为操作系统(OS)。
但是为什么要有操作系统呢,设计操作系统的目的又是什么呢 ?

  1. 与硬件交互,管理所有的软硬件资源
  2. 为用户程序(应用程序)提供一个良好的执行环境

操作系统是一款搞管理的软件。

既然操作系统是管理的软件,那操作系统是如何管理进程的呢 ?进程的信息又放在了什么地方 ?

进程信息被放在一个叫做进程控制块的数据结构中,可以理解为进程属性的集合。
在课本上是叫作PCB,Linux下的PCB叫作task_struct 。它的内部包含了进程的所有信息,包括进程的编号、状态、优先级、程序计数器、上下文数据等等。

进程的信息可以通过 /proc 系统文件夹查看
请添加图片描述

要获取PID为1的进程信息,你需要查看 /proc/1 这个文件夹。

请添加图片描述

查看正在运行的进程信息,用 top或者ps 命令
请添加图片描述

这里是我开了两个窗口,快捷键(Ctrl+shift+n),一个窗口让一个进程运行,一个窗口查看进程是否在运行中,这里可以看到是可以查到的,用 ps 命令。
请添加图片描述

其中,PID是自己的进程标识符,每个进程都有一个唯一的标识符用于区分。PR是优先级,NI是nice值,S是状态。

三、获取进程标识符


#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main()
{
    while(1)
    {
        printf("pid: %d\n", getpid());//获取自己的pid
        printf("ppid: %d\n", getppid());//获取自己父亲的pid
        sleep(1);                                                         
    }
    return 0;
}

请添加图片描述

可以对应着看一下,pid 和 ppid 都是可以对的上的吧。

fork方法创建子进程

通过 man fork 可以查看。

请添加图片描述

 #include <stdio.h>
 #include <sys/types.h>
 #include <unistd.h>
 
int main()
 {
    int ret = fork();
    printf("hello : %d!, ret: %d\n", getpid(), ret);
    sleep(1);
    return 0;
 }

请添加图片描述

一个 printf 语句竟然执行了两条。

#include <stdio.h>
#include <sys/types.h>
 #include <unistd.h>
 
int main()
 {
    int ret = fork();
    if(ret < 0){
        perror("fork()");
        return 1;
    }
    else if(ret == 0){ //child
        printf("child : %d!, ret: %d\n", getpid(), ret);
    }else{ //father
        printf("father : %d!, ret: %d\n", getpid(), ret);
    }
    sleep(1);
    return 0;
 }

请添加图片描述

子进程可以成功进入if 分支,说明它是与父进程共享代码和数据的!一般而言子进程创建出来后就会与父进程共享后面的代码和数据,包括return语句,所以会返回两次。返回不同的值,也是为了区分父子进程,让不同的执行流来执行不同的代码块。

但是进程是有独立性的。子进程和父进程共享代码可以理解,因为程序运行中代码是不能被修改的,但是数据是可以被修改的,如果父子进程共享数据的话,那么一个进程修改数据之后不会影响到另一个进程吗?

这里牵扯到另一个概念,即子进程的写时拷贝。父子进程最初共享同一份数据,如果此时子进程要修改父进程的数据,那么就会发生写时拷贝,即给子进程分配一块新的空间并拷贝原数据,然后进行修改。相比一开始就把全部数据拷贝一份,这种方法能够很好的减少空间浪费。

所以这也解释了为什么变量id能够同时有两个不同的值。

四、 进程状态

进程状态可以反应进程在执行过程中的变化,在操作系统中进程状态可以分为五个基本状态,即新建状态、运行状态、就绪状态、阻塞状态、终止状态。

请添加图片描述

其中最主要的是运行态、就绪态和阻塞态。当一个进程在CPU中被执行时处于运行态,但是操作系统中有很多个进程需要运行,不能让一个进程占用CPU过长时间,于是需要按照时间片来排队执行。当一个进程的运行时间片到了,就需要退出运行态,重新等待CPU的调度,变成就绪态。如果一个进程在执行期间需要等待某个事件,例如进程要访问外设,但是外设没有输入时,进程就要进入外设的等待队列,此时就会进入阻塞态,等待事件的完成。当外设输入后,进程重新回到CPU的运行队列。

实际上还有挂起状态,当操作系统内部的内存资源严重不足时,操作系统就会将一些进程的代码和数据换出到磁盘中,此时进程就处于挂起状态。当需要运行时再将代码和数据从磁盘换入到内存中。

看看Linux内核源代码怎么说

 /*
 * 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 */
 };
  1. R(running):运行状态,进程在被CPU调度或处于运行队列中
  2. S(sleeping):睡眠状态,进程在等待时间完成,也叫做可中断睡眠状态
  3. D(disk sleep):磁盘休眠状态,也叫不可中断睡眠状态,该状态下的进程会等待IO的结束
  4. T(stopped):停止状态,可以通过发送信号让进程停止
  5. t(tracing stop):追踪状态,程序在调试时遇到断点停下的状态
  6. X(dead):死亡状态,进程死亡后被回收的状态
  7. Z(zombie):僵尸状态,进程死亡后未被回收时保持的状态

1、R状态

#include <stdio.h>  
int main()  
{
    while(1)
    {
        ;                                                                                                  
    }
    return 0;
}

请添加图片描述

可以看到此时进程的状态就是R+,这个+代表进程此时在前台运行,如果我们想让进程在后台运行的话,只需要在运行程序时在命令的后面加上 & 即可,此时就可以在运行程序的同时输入其他命令了。

2、S状态

#include <stdio.h>
int main()
{
int a = 0;
scanf("%d",&a); 
return 0;
}

请添加图片描述

可以看到此时进程状态就是S+,进程正在等待外设的输入。

3、D状态

在操作系统的内存处于危急存亡的时刻,将进程挂起也不管用了,这时候操作系统就会开始杀进程。

但是假如一个进程正在向磁盘中写入一些十分重要的数据,在等待磁盘写入完毕的期间会处于阻塞态,如果此时被操作系统杀掉了,就会导致数据丢失进而产生严重的后果。因此针对这种情况设计了深度睡眠状态即D状态,位于该状态的进程无法被操作系统杀掉。

4、T状态

请添加图片描述

可以看到18号信号SIGCONT是继续进程,19号信号SIGSTOP是暂停进程。

请添加图片描述

Ctrl + z 对应19 号信号。

5、t状态

请添加图片描述

可以看到,我们在程序中打了一个断点并开始运行程序,当运行到断点处时程序暂停,此时观察进程状态就会发现该进程正处于t状态。

6、X状态

进程死亡后的状态,这个状态持续的事件十分短暂,我们几乎无法观察到。

7、Z状态

实际上,进程死亡后并不是立即就变为X状态了,而是需要先经过Z状态即僵尸状态。
就是孩子进程结束的时候,父亲没有回收的时候,这个时候就会造成僵尸进程,当一个进程死亡后,需要维护自己的相关资源等待父进程回收并提交进程的死亡信息。如果父进程迟迟不读取子进程的状态,子进程就会一直保持Z状态不能被释放。


#include <stdio.h>
 #include <stdlib.h>
 
int main()
 {
    pid_t ret = fork();
    if(ret < 0){
        perror("fork()");
        return 1;
        }
 else if(ret > 0){
 printf("child : %d!, ret: %d\n", getpid(), ret);
 sleep(30);
}else{
 printf("father : %d!, ret: %d\n", getpid(), ret);
 sleep(5);
 exit(EXIT_SUCCESS);
 }
 return 0;
 }

请添加图片描述

进程的退出状态必须被维持下去,因为他要告诉关心它的进程(父进程),你交给我的任务,我办的怎么样了。可父进程如果一直不读取,那子进程就一直处于Z状态 ? 答案是对的。
那一个父进程创建了很多子进程,就是不回收,是不是就会造成内存资源的浪费?是的!因为数据结构
对象本身就要占用内存,想想C中定义一个结构体变量(对象),是要在内存的某个位置进行开辟空间!

孤儿进程

前面提到,子进程先结束而父进程迟迟不结束,子进程会保持僵尸状态。
如果父进程提前结束,而子进程还在运行,那么子进程就变成了孤儿进程。当子进程结束之后该由谁来回收呢?

#include <stdio.h>
 #include <stdlib.h>
 
int main()
 {
    pid_t ret = fork();
    if(ret < 0){
        perror("fork()");
        return 1;
        }
 else if(ret > 0){
 printf("child : %d!, ret: %d\n", getpid(), ret);
 sleep(5);
}else{
 printf("father : %d!, ret: %d\n", getpid(), ret);
 sleep(30);
 }
 return 0;
 }

请添加图片描述

可以看到,父进程退出后子进程的PPID变为了1,即我们的操作系统。
所以当父进程比子进程先结束时,子进程就会被操作系统领养,最后由操作系统负责回收。

五、进程优先级

cpu资源分配的先后顺序,就是指进程的优先权。
优先权高的进程有优先执行权利。配置进程优先权对多任务环境的 linux 很有用,可以改善系统性能。
还可以把进程运行到指定的CPU上,这样一来,把不重要的进程安排到某个CPU,可以大大改善系统整体性能。

请添加图片描述

  1. UID : 代表执行者的身份
  2. PID : 代表这个进程的代号
  3. PPID :代表这个进程是由哪个进程发展衍生而来的,亦即父进程的代号
  4. PRI :代表这个进程可被执行的优先级,其值越小越早被执行
  5. NI :代表这个进程的nice值

原文地址:https://blog.csdn.net/2402_84602321/article/details/143493283

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!