Linux进程(一)

#新星杯·14天创作挑战营·第10期#

Linux进程(一)

进程的基本概念

课本概念: 程序的一个执行实例,正在执行的程序等
内核观点: 担当分配系统资源(CPU时间,内存)的实体

当代码进行编译链接后便会生成一个可执行程序,这个可执行程序本质上是一个文件,是放在磁盘上的。当我们双击这个可执行程序将其运行起来时,本质上是将这个程序加载到内存当中了,因为只有加载到内存后,CPU才能对其进行逐行的语句执行,而一旦将这个程序加载到内存后,我们就不应该将这个程序再叫做程序了,严格意义上将应该将其称之为进程

在这里插入图片描述

简而言之:一个程序运行起来后就是进程

描述进程-PCB

系统当中可以同时存在大量进程,使用ps aux便可以显示系统当中存在的进程
在这里插入图片描述

ps aux | head -1 && ps aux | grep proc | grep -v grep

使用以上命令就可以得到某一指定进程的基本信息

而当你开机的时候启动的第一个程序就是我们的操作系统(即操作系统是第一个加载到内存的),我们都知道操作系统是做管理工作的,而其中就包括了进程管理。而系统内是存在大量进程的,那么操作系统是如何对进程进行管理的呢?
**先描述,再组织。**操作系统管理进程也是一样的,操作系统作为管理者是不需要直接和被管理者(进程)直接进行沟通的,当一个进程出现时,操作系统就立马对其进行描述,之后对该进程的管理实际上就是对其描述信息的管理
进程信息被放在一个叫做进程控制块的数据结构中,可以理解为进程属性的集合,课本上称之为PCB(process control block)

操作系统将每一个进程都进行描述,形成了一个个的进程控制块(PCB),并将这些PCB以双链表的形式组织起来
在这里插入图片描述

这样一来,操作系统只要拿到这个双链表的头指针,便可以访问到所有的PCB。此后,操作系统对各个进程的管理就变成了对这条双链表的一系列操作
例如创建一个进程实际上就是先将该进程的代码和数据加载到内存,紧接着操作系统对该进程进行描述形成对应的PCB,并将这个PCB插入到该双链表当中。而退出一个进程实际上就是先将该进程的PCB从该双链表当中删除,然后操作系统再将内存当中属于该进程的代码和数据进行释放或是置为无效
总结:操作系统对进程的管理实际上就变成了对该双链表的增、删、查、改等操作

task-struct介绍

Linux操作系统是用C语言进行编写的,那么Linux当中的进程控制块必定是用结构体来实现的

PCB实际上是对进程控制块的统称,在Linux中描述进程的结构体叫做task_struct,task_struct是Linux内核的一种数据结构,它会被装载到RAM(内存)里并且包含进程的信息

task-struct包含信息

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

进程的PID与PPID

  • PID是操作系统为每个进程动态分配的唯一非零整数标识符,用于唯一标识一个正在运行的进程

  • PPID是创建当前进程的父进程的PID。每个进程(除init外)都有一个父进程

通过使用系统调用函数getpid()getppid()即可分别获取进程的PID和PPID

while(true)
{
    printf("I am child,pid: %d,ppid: %d,cnt:%d\n",getpid(),getppid(),cnt);
    sleep(1);
}

在这里插入图片描述

通过系统调用创建进程

fork()函数创建子进程

fork是一个系统调用级别的函数,其功能就是创建一个子进程

int main()
{
  fork();
  while(1)
  {
    printf("I am a process,pid: %d,ppid: %d\n",getpid(),getppid());
    sleep(1);
  }
  return 0;
}

在这里插入图片描述

运行结果是循环打印两行数据,第一行数据是该进程的PID和PPID,第二行数据是代码中fork函数创建的子进程的PID和PPID。我们可以发现fork函数创建的进程的PPID就是proc进程的PID,也就是说proc进程与fork函数创建的进程之间是父子关系

使用fork函数创建子进程,在fork函数被调用之前的代码被父进程执行,而fork函数之后的代码,则默认情况下父子进程都可以执行。需要注意的是,父子进程虽然代码共享,但是父子进程的数据各自开辟空间(采用写时拷贝)

注意: 使用fork函数创建子进程后就有了两个进程,这两个进程被操作系统调度的顺序是不确定的,这取决于操作系统调度算法的具体实现

使用if进行分流

上面说到,fork函数创建出来的子进程与其父进程共同使用一份代码,但我们如果真的让父子进程做相同的事情,那么创建子进程就没有什么意义了。因此,在fork之后我们通常使用if语句进行分流,让父进程和子进程做不同的事

fork函数的返回值:

  • 如果子进程创建成功,在父进程中返回子进程的PID,而在子进程中返回0
  • 如果子进程创建失败,则在父进程中返回 -1

在这里插入图片描述

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

在Linux操作系统当中我们可以通过 ps auxps axj 命令查看进程的状态

在这里插入图片描述

运行状态-R

一个进程处于运行状态(running),并不意味着进程一定处于运行当中,运行状态表明一个进程要么在运行中,要么在运行队列里。也就是说,可以同时存在多个R状态的进程

所有处于运行状态,即可被调度的进程,都被放到运行队列当中,当操作系统需要切换进程运行时,就直接在运行队列中选取进程运行

浅度睡眠状态-S

一个进程处于浅度睡眠状态(sleeping),意味着该进程正在等待某件事情的完成,处于浅度睡眠状态的进程随时可以被唤醒,也可以被杀掉,这里的睡眠有时候也可叫做可中断睡眠(interruptible sleep)

例如在代码中我们经常会使用到sleep()这个函数,当执行这个函数后,进程就进入了浅度睡眠状态,该进程可以被唤醒,也可以被杀死(kiil -9 pid)

深度睡眠状态-D

一个进程处于深度睡眠状态(disk sleep),表示该进程不会被杀掉,即便是操作系统也不行,只有该进程自动唤醒才可以恢复。该状态有时候也叫不可中断睡眠状态(uninterruptible sleep),处于这个状态的进程通常会等待IO的结束

例如,某一进程要求对磁盘进行写入操作,那么在磁盘进行写入期间,该进程就处于深度睡眠状态,是不会被杀掉的,因为该进程需要等待磁盘的回复(是否写入成功)以做出相应的应答

暂停状态-T

在Linux当中,我们可以通过发送SIGSTOP信号使进程进入暂停状态(stopped),发送SIGCONT信号可以让处于暂停状态的进程继续运行

对一个进程发送SIGSTOP信号,该进程就进入到了暂停状态

在这里插入图片描述

我们再对该进程发送SIGCONT信号,该进程就继续运行
在这里插入图片描述

僵尸状态-Z

当一个进程将要退出的时候,在系统层面,该进程曾经申请的资源并不是立即被释放,而是要暂时存储一段时间,以供操作系统或是其父进程进行读取,如果退出信息一直未被读取,则相关数据是不会被释放掉的,一个进程若是正在等待其退出信息被读取,那么我们称该进程处于僵尸状态(zombie)

僵尸状态存在的必要性

进程被创建的目的就是完成某项任务,那么当任务完成的时候,调用方是应该知道任务的完成情况的,所以必须存在僵尸状态,使得调用方得知任务的完成情况,以便进行相应的后续操作例如,我们写代码时都在主函数最后返回0,这个0就是返回给操作系统的,告诉操作系统代码顺利执行结束。在Linux操作系统当中我们可以通过使用echo $?命令获取最近一次进程退出时的退出码

死亡状态-X

死亡状态只是一个返回状态,当一个进程的退出信息被读取后,该进程所申请的资源就会立即被释放,该进程也就不存在了,所以你不会在任务列表当中看到死亡状态(dead)

僵尸进程

前面说到,一个进程若是正在等待其退出信息被读取,那么我们称该进程处于僵尸状态。而处于僵尸状态的进程,我们就称之为僵尸进程

例如,对于以下代码,fork函数创建的子进程在打印5次信息后会退出,而父进程会一直打印信息。也就是说,子进程退出了,父进程还在运行,但父进程没有读取子进程的退出信息,那么此时子进程就进入了僵尸状态

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
	printf("I am running...\n");
	pid_t id = fork();
	if(id == 0){ //child
		int count = 5;
		while(count){
			printf("I am child...PID:%d, PPID:%d, count:%d\n", getpid(), getppid(), count);
			sleep(1);
			count--;
		}
		printf("child quit...\n");
		exit(1);
	}
	else if(id > 0){ //father
		while(1){
			printf("I am father...PID:%d, PPID:%d\n", getpid(), getppid());
			sleep(1);
		}
	}
	else{ //fork error
	}
	return 0;
} 

运行该代码后,我们可以通过以下监控脚本,每隔一秒对该进程的信息进行检测

while :; do ps axj | head -1 && ps axj | grep proc | grep -v grep;echo "######################";sleep 1;done

检测后即可发现,当子进程退出后,子进程的状态就变成了僵尸状态
在这里插入图片描述

僵尸进程的危害

  • 僵尸进程的退出信息被保存在task_struct(PCB)中,僵尸状态一直不退出,那么PCB就一直需要进行维护

  • 若是一个父进程创建了很多子进程,但都不进行回收,那么就会造成资源浪费,因为数据结构对象本身就要占用内存

  • 僵尸进程申请的资源无法进行回收,那么僵尸进程越多,实际可用的资源就越少,也就是说,僵尸进程会导致内存泄漏

孤儿进程

在Linux当中的进程关系大多数是父子关系,若子进程先退出而父进程没有对子进程的退出信息进行读取,那么我们称该进程为僵尸进程。但若是父进程先退出,那么将来子进程进入僵尸状态时就没有父进程对其进行处理,此时该子进程就称之为孤儿进程
若是一直不处理孤儿进程的退出信息,那么孤儿进程就会一直占用资源,此时就会造成内存泄漏。因此,当出现孤儿进程的时候,孤儿进程会被1号init进程领养,此后当孤儿进程进入僵尸状态时就由int进程进行处理回收

例如,对于以下代码,fork函数创建的子进程会一直打印信息,而父进程在打印5次信息后会退出,此时该子进程就变成了孤儿进程

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
	printf("I am running...\n");
	pid_t id = fork();
	if(id == 0){ //child
		int count = 5;
		while(1){
			printf("I am child...PID:%d, PPID:%d\n", getpid(), getppid(), count);
			sleep(1);
		}
	}
	else if(id > 0){ //father
		int count = 5;
		while(count){
			printf("I am father...PID:%d, PPID:%d, count:%d\n", getpid(), getppid(), count);
			sleep(1);
			count--;
		}
		printf("father quit...\n");
		exit(0);
	}
	else{ //fork error
	}
	return 0;
} 

观察代码运行结果,在父进程未退出时,子进程的PPID就是父进程的PID,而当父进程退出后,子进程的PPID就变成了1,即子进程被1号进程领养

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值