[Linux](6)进程的概念,查看进程,创建子进程,进程状态,进程优先级

进程概念

  • 进程是正在运行的程序的实例,它由程序数据进程控制块三部分组成。
  • 进程信息被放在进程控制块中,简称PCB(process control block)
  • Linux操作系统下的PCB是: task_struct ,它是一种结构体。
  • 所有运行在系统里的进程都以 task_struct 链表的形式存在内存中,程序的代码和数据也会被加载到内存中。

其实计算机管理硬件的一个方法就是先描述,再组织。用struct描述被管理的对象,用特定的数据结构再组织起来。

task_struct 内容分类

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

查看进程

一、

显示所有进程

ps ajx

我们也可以先写一个 c 程序

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

int main()
{
    while (1)
    {
        printf("I am a process\n");
        sleep(1);
    }
    return 0;
}

运行起来之后,右击选项卡,选择复制SSH渠道

在新的终端中使用 ps ajx | grep 'mytest' 可以找到自己的这个进程

[CegghnnoR@VM-4-13-centos 2022_8_12]$ ps ajx | grep 'mytest'
 2888 25600 25600  2888 pts/0    25600 S+    1001   0:00 ./mytest
25432 28960 28959 25432 pts/1    28959 S+    1001   0:00 grep --color=auto mytest

但是这里显示了两行,第一行当然是我们自己写的程序,第二行的是 grep 命令执行的进程。


每个进程在系统中,都会存在一个唯一的标识符,称为 pid

通过下面的指令可以查看pid:

[CegghnnoR@VM-4-13-centos 2022_8_12]$ ps ajx | head -1 && ps ajx | grep 'mytest' | grep -v grep
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
 2888 25600 25600  2888 pts/0    25600 S+    1001   0:00 ./mytest

head -1 显示第一行(表头),&& 相当于逻辑与,前面的指令执行成功就执行后面的指令,后面就是找到 mytest 进程,去掉 grep 命令执行的进程。


二、

/proc 下,实时存在着当前的进程信息:

通过 pid 可以找到某个进程,比如 25600 是我们 mytest 程序的进程:

[CegghnnoR@VM-4-13-centos 2022_8_12]$ ls /proc/25600
attr        coredump_filter  gid_map    mountinfo   oom_score      sched      statm
autogroup   cpuset           io         mounts      oom_score_adj  schedstat  status
auxv        cwd              limits     mountstats  pagemap        sessionid  syscall
cgroup      environ          loginuid   net         patch_state    setgroups  task
clear_refs  exe              map_files  ns          personality    smaps      timers
cmdline     fd               maps       numa_maps   projid_map     stack      uid_map
comm        fdinfo           mem        oom_adj     root           stat       wchan

在这之中可以看到 cwd 就是该进程当前的工作路径,exe 就是进程对应的可执行程序的磁盘文件:

[CegghnnoR@VM-4-13-centos 2022_8_12]$ ls -al /proc/25600
total 0
//...

lrwxrwxrwx   1 CegghnnoR CegghnnoR 0 Aug 12 17:35 cwd -> /home/CegghnnoR/code/2022_8_12
-r--------   1 CegghnnoR CegghnnoR 0 Aug 12 19:58 environ
lrwxrwxrwx   1 CegghnnoR CegghnnoR 0 Aug 12 17:35 exe -> /home/CegghnnoR/code/2022_8_12/mytest

//...

通过系统调用获取标识符pid/ppid

系统调用和库函数的概念

在开发角度,操作系统对外会表现为一个整体,但是会暴露自己的部分接口,供上层开发使用,这部分由操作系统提供的接口,叫做系统调用。
系统调用在使用上,功能比较基础,对用户的要求相对也比较高,所以,有心的开发者可以对部分系统调用进行适度封装,从而形成库,有了库,就很有利于更上层用户或者开发者进行二次开发。


getpid 获取当前进程的 pid,头文件 <sys/types.h>

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

int main()
{
    while (1)
    {
        printf("I am a process, pid:%d\n", getpid());
        sleep(1);
    }
    return 0;
}
[CegghnnoR@VM-4-13-centos 2022_8_12]$ ./mytest
I am a process, pid:3997
I am a process, pid:3997
//...

要杀掉该进程,除了使用 ctrl+c,也可以使用 kill -9 [pid]


使用 getppid 可以获取父进程id(ppid)

[CegghnnoR@VM-4-13-centos 2022_8_12]$ ./mytest
I am a process, pid:6008, ppid:4583
^C
[CegghnnoR@VM-4-13-centos 2022_8_12]$ ./mytest
I am a process, pid:6023, ppid:4583
^C
[CegghnnoR@VM-4-13-centos 2022_8_12]$ ./mytest
I am a process, pid:6040, ppid:4583
^C
[CegghnnoR@VM-4-13-centos 2022_8_12]$ ./mytest
I am a process, pid:6042, ppid:4583
^C

每次启动进程,pid都会发生变化,ppid不变。

因为几乎我们在命令行上所执行的所有的指令,都是 bash 进程的子进程。

代码创建子进程 fork()

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

int main()
{
    pid_t id = fork();
    printf("hello world id:%d\n", id);
    return 0;
}

结果:

hello world id:6243
hello world id:0

printf执行了两次,id为0的是子进程,>0的是父进程。

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

int main()
{
    pid_t id = fork();
    if (id == 0)
    {
        while (1)
        {
            printf("我是子进程,pid:%d,我的父进程pid:%d\n", getpid(), getppid());
            sleep(1);
        }
    }
    else
    {
        while (1)
        {
          printf("我是父进程,pid:%d,我的父进程pid:%d\n", getpid(), getppid());
          sleep(1);
        }
    }
    return 0;
}
[CegghnnoR@VM-4-13-centos 2022_8_12]$ ./mytest
我是父进程,pid:9668,我的父进程pid:2170
我是子进程,pid:9669,我的父进程pid:9668
  • fork 之后,父进程和子进程会共享代码,一般都会执行后续的代码。
  • fork 之后,父进程和子进程返回值不同,给父进程的返回值是子进程的pid,子进程的返回值是0可以通过判断不同的返回值让父子进程执行不同的代码块。

进程状态

概念

运行状态:进程只要在运行队列中就处于运行状态,代表已经准备好随时被调度。

终止状态:进程还在,只不过永远不运行了,随时等待被释放。

因为释放也占用资源,所以终止状态存在的意义就是,在操作系统较忙时可以先安排其他更重要的工作,稍后对终止状态的进程进行释放

阻塞状态

  • 进程不仅需要向 CPU 申请资源,也可能会向磁盘、网卡、显卡等申请资源。如果向CPU申请资源暂时无法满足,则会在运行队列中排队。如果向其他设备申请资源,也是需要排队的。

  • 当进程需要访问某些资源(磁盘网卡等)时,该资源如果暂时没有准备好,或者正在给其他进程提供服务,此时要将当前进程从运行队列中移除放到相应设备的等待队列。这样 CPU 就可以继续运行后面的进程,提高效率。(底层其实就是 PCB 的移动)

在这个等待过程中,该进程的代码不会被执行,看上去就和卡住了一样,这被称为进程阻塞

挂起状态

如果内存不足,而有一些进程短时间内不会被调度(等待的资源短时间内不会就绪),它的代码和数据还存在内存中就是一种浪费,操作系统就会把该进程的代码和数据置换到磁盘上,只留 PCB 在内存中,这样的进程就处于挂起状态。

Linux 进程状态

以上是操作系统原理中的概念,具体到 Linux 系统中稍微有一点不一样。

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 */
};

写一个不停打印 Hello world 的程序:

#include <stdio.h>

int main()
{
    while (1)
    {
        printf("Hello world\n");
    }
    return 0;
}

运行后查看该进程的状态:

[CegghnnoR@VM-4-13-centos file]$ ps ajx | head -1 && ps ajx | grep 'mytest' | grep -v 'grep'
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
 2170 26046 26046  2170 pts/0    26046 S+    1001   0:08 ./mytest

STAT一栏显示的就是该进程的状态,为S+,S代表睡眠的意思,其实就是阻塞状态,但是这边明明正在不停地打印Hello world,为什么不是运行状态呢?

其实该程序要运行的代码就一行printf,CPU的速度是极快的,但是显示器的速度慢,绝大部分时间进程都在等待显示器的资源。

如果你把printf() 一行去掉,再运行看程序状态:

#include <stdio.h>

int main()
{
    while (1)
    {
    }
    return 0;
}
[CegghnnoR@VM-4-13-centos file]$ ps ajx | head -1 && ps ajx | grep 'mytest' | grep -v 'grep'
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
 2170 29435 29435  2170 pts/0    29435 R+    1001   0:11 ./mytest

这个进程不用访问其他外设,只等 CPU 的资源,而且是死循环,状态显示R+,也就是运行状态。

S状态也叫浅度睡眠,或可中断睡眠,意为可以随时被唤醒,或者手动去终止它。

D状态也是一种阻塞状态,一般指等待磁盘资源。该状态也叫深度睡眠,或不可中断睡眠,因为磁盘的写入删除都要返回一个成功或失败的反馈信息,在这期间该进程必须等待,如果被杀,会导致错误,所以D状态是对该进程的一种保护,使它不可被杀掉。

X状态是死亡状态,也就是上面说的终止状态。

Z状态是僵尸状态,当一个Linux中的进程退出的时候,一般不会直接进入X状态,而是进入Z状态,这是为了维护进程的 PCB 中的退出信息,从而让父进程或者操作系统读取。

僵尸进程的模拟及其危害

用fork创建一个子进程,然后子进程正常运行5s后退出,父进程一直死循环。

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

int main()
{
    pid_t id = fork();
    if (id == 0)
    {
        int  cnt = 5;
        while (cnt)
        {
            printf("我是子进程,我还剩%ds\n", cnt--);
            sleep(1);
        }
        printf("我是子进程,我已经变僵尸了,等待被检测\n");
        exit(0);
    }
    else
    {
        while (1)
        {
            sleep(1);
        }
    }
    return 0;
}

结果:

[CegghnnoR@VM-4-13-centos 2022_8_12]$ ./mytest
我是子进程,我还剩5s
我是子进程,我还剩4s
我是子进程,我还剩3s
我是子进程,我还剩2s
我是子进程,我还剩1s
我是子进程,我已经变僵尸了,等待被检测
^C

同时在另一个终端中使用指令每隔一秒查看这对父子进程的状态:

[CegghnnoR@VM-4-13-centos 2022_8_12]$ while :; do ps ajx | head -1 && ps ajx | grep 'mytest' | grep -v 'grep'; sleep 1; echo "###################################"; done
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
26059 29858 29858 26059 pts/1    29858 S+    1001   0:00 ./mytest
29858 29862 29858 26059 pts/1    29858 S+    1001   0:00 ./mytest
###################################
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
26059 29858 29858 26059 pts/1    29858 S+    1001   0:00 ./mytest
29858 29862 29858 26059 pts/1    29858 S+    1001   0:00 ./mytest
###################################
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
26059 29858 29858 26059 pts/1    29858 S+    1001   0:00 ./mytest
29858 29862 29858 26059 pts/1    29858 S+    1001   0:00 ./mytest
###################################
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
26059 29858 29858 26059 pts/1    29858 S+    1001   0:00 ./mytest
29858 29862 29858 26059 pts/1    29858 S+    1001   0:00 ./mytest
###################################
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
26059 29858 29858 26059 pts/1    29858 S+    1001   0:00 ./mytest
29858 29862 29858 26059 pts/1    29858 S+    1001   0:00 ./mytest
###################################
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
26059 29858 29858 26059 pts/1    29858 S+    1001   0:00 ./mytest
29858 29862 29858 26059 pts/1    29858 Z+    1001   0:00 [mytest] <defunct>	#变成僵尸状态
###################################
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
26059 29858 29858 26059 pts/1    29858 S+    1001   0:00 ./mytest
29858 29862 29858 26059 pts/1    29858 Z+    1001   0:00 [mytest] <defunct>
###################################
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
26059 29858 29858 26059 pts/1    29858 S+    1001   0:00 ./mytest
29858 29862 29858 26059 pts/1    29858 Z+    1001   0:00 [mytest] <defunct>
###################################
^C

可以看到,5s后子进程变成了僵尸进程。

如果父进程一直不回收僵尸状态的子进程,那么该状态就一直维护着,该进程的相关资源不会被释放,造成内存泄漏!

孤儿进程

顾名思义,就是父进程提前退出,子进程还在运行。

父进程退出有bash进程回收,然后子进程会被1号进程(操作系统)领养。子进程退出会被操作系统回收。

模拟孤儿进程:

父进程运行三秒后退出,子进程死循环

#include <stdio.h>                                                                                   
#include <stdlib.h>                                                                                  
#include <unistd.h>                                                                                  
                                                                                                     
int main()                                                                                           
{                                                                                                    
    pid_t id = fork();                                                                               
    if (id == 0)                                                                                     
    {                                                                                                
        while (1)  
        {  
            printf("我是子进程\n");  
            sleep(1);  
        }                                                                                                
    }                                                   
    else                                       
    {                                          
        int cnt = 3;                           
        while (cnt)                            
        {                                      
            printf("我是父进程,我:%d\n", cnt--);  
            sleep(1);                          
        }                                      
        exit(0);                               
    }                                          
    return 0;                                  
} 
[CegghnnoR@VM-4-13-centos 2022_8_12]$ while :; do ps ajx | head -1 && ps ajx | grep 'mytest' | grep -v 'grep'; sleep 1; echo "###################################"; done
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
26059  3847  3847 26059 pts/1     3847 S+    1001   0:00 ./mytest
 3847  3848  3847 26059 pts/1     3847 S+    1001   0:00 ./mytest
###################################
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
26059  3847  3847 26059 pts/1     3847 S+    1001   0:00 ./mytest
 3847  3848  3847 26059 pts/1     3847 S+    1001   0:00 ./mytest
###################################
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
26059  3847  3847 26059 pts/1     3847 S+    1001   0:00 ./mytest
 3847  3848  3847 26059 pts/1     3847 S+    1001   0:00 ./mytest
###################################
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
    1  3848  3847 26059 pts/1    26059 S     1001   0:00 ./mytest
###################################
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
    1  3848  3847 26059 pts/1    26059 S     1001   0:00 ./mytest

👆🏻:3 秒后父进程消失,子进程的 ppid 变成 1,表示被操作系统领养了。

另外,子进程的状态由 S+ 变为 S,有 + 号的表示前台进程,无 + 号的表示后台进程,前台进程可以使用 ctrl+c 终止,后台进程只能用 kill -9 [pid] 的方式终止。

暂停状态

T状态t状态都表示暂停。

T状态表示常规的暂停,比如 kill -19 [pid] 暂停的进程:

[CegghnnoR@VM-4-13-centos 2022_8_12]$ ps ajx | head -1 && ps ajx | grep mytest | grep -v grep
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
26059  7716  7716 26059 pts/1     7716 S+    1001   0:04 ./mytest
[CegghnnoR@VM-4-13-centos 2022_8_12]$ kill -19 7716	#输入暂停命令
[CegghnnoR@VM-4-13-centos 2022_8_12]$ ps ajx | head -1 && ps ajx | grep mytest | grep -v grep
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
26059  7716  7716 26059 pts/1    26059 T     1001   0:07 ./mytest	#变为T状态

另:使用 kill -18 [pid] 可以让进程继续执行。使用 kill -l 可以查看选项。

t状态表示进程被调试的时候,遇到断点时所处的状态。

[CegghnnoR@VM-4-13-centos 2022_8_12]$ ps ajx | head -1 && ps ajx | grep mytest | grep -v grep
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
26059  9465  9465 26059 pts/1     9465 S+    1001   0:00 gdb mytest
 9465  9602  9602 26059 pts/1     9465 t     1001   0:00 /home/CegghnnoR/code/2022_8_12/mytest

总结

  • 运行状态对应R状态
  • 终止状态对应Z状态和X状态
  • 阻塞状态对应S状态或D状态
  • 挂起状态对应S状态或D状态或T/t状态

这就是操作系统原理和 Linux 具体实现的区别。

进程优先级

进程优先级就是各个进程获取某种资源的先后顺序。

[CegghnnoR@VM-4-13-centos 2022_8_12]$ ps -al
F S   UID   PID  PPID  C PRI  NI ADDR SZ WCHAN  TTY          TIME CMD
0 S  1001 14456 26059  6  80   0 -  1833 n_tty_ pts/1    00:00:00 mytest
0 R  1001 14474  2170  0  80   0 - 38595 -      pts/0    00:00:00 ps

Linux 下进程优先级由两部分组成:PRI 和 NI,数字越小,代表优先级越高,数字越大,代表优先级越低。

默认优先级是80,要更改进程优先级就要更改 NI,而不是更改 PRI。NI 表示进程优先级的修正数据。

NI的取值范围是-20~19,一共40个级别,PRI = 默认优先级 + NI

用 top 命令可以更改已存在进程的 NI 值

  • top
  • 进入 top 后按 r --> 输入进程 pid --> 输入 NI 值

其他概念

竞争性:系统进程数目众多,而 CPU 资源只有少量,所以进程之间具有竞争性,为了高效完成任务,更合理竞争资源,便有了优先级。

独立性:多进程运行,需要独享各种资源,多进程运行期间互不干扰(不会因为一个进程挂掉或者异常,而导致其他进程出现问题)。

并行:多个进程在多个 CPU 下同时运行。

并发:多个进程在一个 CPU 下采用进程切换的方式,在一段时间之内,让多个进程都得以推进。

一个 CPU 一次只能运行一个进程,其他进程都在运行队列里等待,由于并发的存在,才得以让我们在宏观上看像是多个进程在同时运行。

进程抢占:对于正在 CPU 上运行的进程,如果此时来了个更高优先级的进程,调度器会直接把低优先级进程从 CPU 上剥离,放上更高优先级的进程。

我们知道队列是不允许插队的,在这种情况下又想实现优先级该怎么办呢?

实际上一个CPU有多个运行队列,把相同优先级的进程放在同一个队列中,CPU会先选择优先级高的队列里的进程开始运行。

进程在运行中产生的各种寄存器数据,叫做上下文数据,当进程被剥离时需要保存上下文数据,当进程恢复的时候,需要将曾经保存的上下文数据恢复到寄存器中。上下文数据保存在 PCB 中。

评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

世真

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值