Linux——进程再识,进程状态、创建与写实拷贝

Linux下每个进程都会有一个非负整数来表示唯一的进程,每个进程都会有自己的父进程,父进程又会有自己的父进程,最终都会追溯到一号进程进 init 进程,这就决定了操作系统上所有的进程都会组成树状结构,就像一个家庭的家谱一样。可以通过pstree来查看进程的家谱图,
这里写图片描述
这样就必须保证操作系统不能同时出现两个相同的进程,虽然进程ID是唯一的,但是进程ID可以重用,进程退出后,其进程ID也可以再次分配给其他进程使用。

// 内核是如何分配进程ID的呢?

分配时采用了延迟重用的算法,分配给新创建的ID尽量不要与最近终止的进程的ID相同,这样就避免了将新创建的进程误判为使用相同进程的ID的已经退出的进程

//实现延迟重用

1、位图记录进程ID的分配情况(1为已占用,0为可用)

2、将上次分配的进程的ID记录到last_pid中,分配进程ID时,从last_pid+1开始找起

3、如果找到位图集合的最后一位仍不可用,则回滚到位图集合的起始位置,从头开始找(从300开始找,因为300以下的pid会被系统占用,不能分配给用户进程)

//  由于位图记录进程ID,则位图肯定存在大小,位图的最大的个数在系统中称为pid_max,他的值可通过procfs或者sysctl查看,也可被修改

进程状态分类:

R (TASK_RUNNING),可执行状态。

只有在该状态的进程才可能在CPU上运行。而同一时刻可能有多个进程处于可执行状态,这些进程的task_struct结构(进程控制块)被放入对应CPU的可执行队列中(一个进程最多只能出现在一个CPU的可执行队列中)

S (TASK_INTERRUPTIBLE),可中断的睡眠状态

处于这个状态的进程因为等待某某事件的发生(比如等待socket连接、等待信号量),而被挂起。这些进程的task_struct结构被放入对应事件的等待队列中。当这些事件发生时(由外部中断触发、或由其他进程触发),对应的等待队列中的一个或多个进程将被唤醒

D (TASK_UNINTERRUPTIBLE),不可中断的睡眠状态

与TASK_INTERRUPTIBLE状态类似,进程处于睡眠状态,但是此刻进程是不可中断的。不可中断,指的并不是CPU不响应外部硬件的中断,而是指进程不响应异步信号,在睡眠期间 ,不能被杀死。

//进程的创建

这里写图片描述


模拟实现结果
这里写图片描述


T (TASK_STOPPED or TASK_TRACED),暂停状态或跟踪状态

暂停:进程收到某信号,运行被停止
跟踪:与暂停状态类似,进程被停止,被另一进程跟踪

Z (TASK_DEAD - EXIT_ZOMBIE),退出状态,进程成为僵尸进程

在这个退出过程中,进程占有的所有资源将被回收,除了task_struct结构(以及少数资源)以外。于是进程就只剩下task_struct这么个空壳,故称为僵尸。
之所以保留task_struct,是因为task_struct里面保存了进程的退出码、以及一些统计信息。而其父进程很可能会关心这些信息。比如在shell中,$?变量就保存了最后一个退出的前台进程的退出码,而这个退出码往往被作为if语句的判断条件。
当然,内核也可以将这些信息保存在别的地方,而将task_struct结构释放掉,以节省一些空间。但是使用task_struct结构更为方便,因为在内核中已经建立了从pid到task_struct查找关系,还有进程间的父子关系。释放掉task_struct,则需要建立一些新的数据结构,以便让父进程找到它的子进程的退出信息。

父进程可以通过wait系列的系统调用(如wait4、waitid)来等待某个或某些子进程的退出,并获取它的退出信息。然后wait系列的系统调用会顺便将子进程的尸体(task_struct)也释放掉。

模拟实现僵尸进程:
#include<stdio.h>
#include<stdlib.h>
#include<sys/types.h>
#include<unistd.h>
int main ()
{
    pid_t pid;
    pid = fork();
    if(pid<0)
    {
       printf("error\n");
    }
    else if(pid == 0)
    {
       //子进程
       exit(0);
    }
    else
    {   //父进程
     sleep(300);  //休眠300秒
     wait(NULL);   //获取僵尸进程的退出信息
    }
}

清除僵尸进程的两种方法:
1、父进程调用wait函数,为子进程“”收尸“”
2、父进程退出,init 进程为子进程“”收尸“”

孤儿进程:
孤儿进程是没有父进程的进程,孤儿进程这个重任就落到了init进程身上,init进程就好像是一个民政局,专门负责处理孤儿进程的善后工作。每当出现一个孤儿进程的时候,内核就把孤 儿进程的父进程设置为init,而init进程会循环地wait()它的已经退出的子进程。这样,当一个孤儿进程凄凉地结束了其生命周期的时候,init进程就会代表党和政府出面处理它的一切善后工作。因此孤儿进程并不会有什么危害。

//模拟实现孤儿进程

这里写图片描述

这里写图片描述

X (TASK_DEAD - EXIT_DEAD),退出状态,进程即将被销毁

真正死亡的状态,进程停留在该状态的时间极短,很难观察

进程的创建:
Linux下,进程可以调用fork函数来创建新进程,调用进程的为父进程,被创建的进程为子进程。

//接口定义
#include<unistd.h>
pid_t fork(void);

fork 函数会返回两次,向子进程返回0 ,并将子进程的ID返给父进程,当然,fork失败,则会返回-1,并设置 error。

//一般来说,fork 程序处理方法:
pid = fork();
if(pid == 0)
{
     //子进程代码
}
else if(pid > 0)
{
 // 父进程代码
}
else
{
      //fork失败,执行error
}
//进程创建之后,对于父子进程,谁先获取CPU资源呢?
     这个没有特定的顺序。

fork之后父子进程的内存关系:
当我们为一个进程刻意设置三个变量,然后去fork一个子进程 会发现子进程也存在父进程所有的内容,当我们对子进程的三个变量进行修改时,发现并不会影响父进程变量。

1、位于数据段的全局变量
2、位于栈上的局部变量
3、位于堆上的动态开辟的内存

那子进程是怎么得到父进程的代码段的呢?
fork时完全copy父进程的数据段,栈和堆?这明显不可能,因为fork之后,子进程几乎总是通过调用exec系列函数来执行新的程序,而exec函数会会丢弃现有的程序代码段(copy的内存),这就有了Linux写实拷贝的技术(copy-on-write )

//写实拷贝

指子进程的页表项指向与父进程相同的物理内存页,这样只要copy父进程的页表项就可以了,当然还要必须把这些页面标记为只读,如果父子进程都不修改内存内容,则共用一分物理内存页,要是父子进程中有任何一方想要修改,内核会试图为该物理页面创建一份新的页面,并将内容全部拷贝到里面,让父子进程各自拥有一分独立的物理内存。
这里写图片描述

普通进程优先级:
Linux下都会存在一个nice值,取值范围为(-20 ---- 19 ),nice值越高,表示优先级越低其中默认优先级为 0。

调整已存在的进程

renice -10 -p 3049(进程代号)
   //将进程pid  3049  的nice值设置为 -10 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值