上机-通过打印进程控制块中的信息深刻认识进程
操作系统为了对进程进行管理,我们就必须对每个进程在其生命周期内涉及的所有事情进行全面的描述,那么对于这些信息在内核中的存储我们引入了task_struck结构体即进程控制块PCB,那么我们先来了解一下它的所有域按照其功能划分类别如下:
top命令查看进程相关信息
分析上图
top - 07:30:02 up 7 min, 1 user, load average: 1.86, 1.99, 1.08
内容 | 意义 |
---|---|
07:30:02 | 当前时间 |
up 7 min | 系统远行时间7分 |
1 user | 当前登陆用户数 |
load average: 1.86, 1.99, 1.08 | 系统负载(任务队列的平均长度)。 三个数值分别为 1分钟、5分钟、15分钟前到现在的平均值 |
Tasks:297 total, 1 running, 296 sleeping, 0 stopped, 0 zombie
内容 | 意义 |
---|---|
Tasks: 297 total | 进程总数297 |
1 running | 正在运行的进程数为1 |
296 sleeping | 睡眠的进程数296 |
0 stopped | 停止的进程数0 |
0 zombie | 僵尸进程数0 |
Cpu(s): 4.3 us, 17.3 sy, 5.5 ni,36.9 id, 33.1 wa, 0.0 hi, 2.9 si, 0.0 st
内容 | 意义 |
---|---|
4.3 us | 用户空间占cpu比例 |
17.3 sy | 内核空间占cpu比例 |
5.5 ni | 用户进程空间内改变过优先级的进程占用CPU比例 |
36.9 id | 空闲cpu比例 |
33.1 wa | 等待输入输出的CPU时间比例 |
0.0 hi | 硬中断占cpu比例 |
2.9 si | 软中断占cpu比例 |
0.0 st | 被虚拟机偷掉的cpu时间比例 |
MiB Mem: 1923.3 total, 65.0 free, 1133.8 used,724.5 buffers/cache
内容 | 意义 |
---|---|
1923.3 total | 物理内存总量 |
65.0 free | 空闲内存总量 |
1133.8 used | 使用的物理内存总量 |
724.5 buffers | 用作内核缓存的内存量 |
MiB Swap:6046.0 total, 6013.8 free, 32.2 used, 594.2 avail Mem
内容 | 意义 |
---|---|
6046.0 total | 交换区总量 |
6013.8 free | 空闲交换区总量 |
32.2 used | 使用的交换区总量 |
594.2 avail Mem | 进程下一次分配的物理内存数量 |
进程id为3006;当前进程用户名first;PR优先级;NI为19>0即低优先级;进程使用的虚拟内存量是720808;进程使用的、未被换出的物理内存大小为103620kb;共享内存大小为14464kb;当前进程状态为S说明此进程处于睡眠状态;(D不可中断的睡眠状态、R运行、S睡眠、T=跟踪、 Z僵尸进程);上次更新到现在的CPU时间占用28.3%;当前进程使用的物理内存5.3%;当前进程所用的时间总计49分钟4秒即2944秒。
打印task_struct中的字段信息
每当进程从用户态进入内核态都要使用进程内核栈,一旦进程进入内核态,cpu就会自动设置该进程的内核栈(内核的数据段),为了节省空间,我们把内核栈和thread_info放在一起,因为从用户态切换到内核态以后,内核态是空的,所以ESP堆栈寄存器直接指向内存区顶端,当我们向栈中写入数据时,ESP就会递减,而task_struct和thread_info都有一个域指向对方,由于PCB的内容越来越大,所以仅将thread_info放在内核栈空间中。内核栈在申请的时候,总是 8K 对齐的,也就是说地址的低12位肯定为0,所以通过esp&~(8k-1)可以获得内核栈的基址,而 thread_info 存在于内核栈的栈底处,所以也就获取到了该进程对应的 thread_info 结构。
我们通过current_thread_info()函数,来获取thread_info结构的基地址,执行后就指向当前进程的thread_info结构,将其封装为current宏来找到当前进程的task_struct,其本质上等价于current_thread_info()->task。
task_struct结构位于sched.h头文件中,路径为/include/linux/sched.h,分析task_struct源码可以看到涉及的字段信息很多,我们选择几个字段信息进行打印。
调试代码以及结果:
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/sched.h>//task结构体
#include <linux/fdtable.h>//files
#include <linux/fs_struct.h>//fs
#include <linux/mm_types.h>//打印内存信息
#include <linux/init_task.h>
#include <linux/types.h>
#include <linux/atomic.h>
MODULE_LICENSE("GPL");//许可证
static int target_pid = 0;//指定的进程号
module_param(target_pid, int, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
MODULE_PARM_DESC(target_pid, "指定的进程号");
static int __init print_pcb(void)
{
struct task_struct *task,*p;
struct list_head *pos;//双向链表
int count=0;//统计当前系统进程一共有多少个
printk("begin...\n");//对链表遍历时,希望从第一个开始
task=&init_task;//指向0号进程pcb
list_for_each(pos,&task->tasks) //遍历操作,使用pos指向,传入的参数task指向tasks字段.0号进程的tasks进行遍历。tasks将所有的进程连在一块。
{
p=list_entry(pos,struct task_struct,tasks);//找到一个节点,就可以用这个节点的tasks字段,找到这个结构体的地址.对应的字段tasks
//此时的p指针已经指向task_struct结构体的首部,后面就可以通过p指针进行操作
count++;//找到一个进程,自加
printk("\n\n");
printk("进程pid: %d","进程状态state: %d;进程标记flags:%d;进程调度优先级prio;进程创建时间:%lld;进程退出状态码:%d;进程调度策略policy:%d;进程调度静态优先级static_prio:%d;父进程parent_pid: %d;共享表进程数: %d;文件权限umask: %d\n",p->id,p->__state,p->flags,p->prio,p->start_time,p->exit_code,p->policy,p->static_prio,(p->parent)->pid,atomic_read(&(p->files)->count),(p->fs)->umask);//linux中内核线程的mm是空的,要对它进行打印,就会出错,指针错误
if((p->mm)!=NULL)
printk("Total_vm: %ld\n",(p->mm)->total_vm); //线性区总的页数
}
printk("进程的个数:%d\n",count);
printk("\n******************************************");
printk("如下是指定进程%d的相关信息:\n",target_pid);
task=&init_task;
list_for_each(pos,&task->tasks) //遍历操作,使用pos指向,传入的参数task指向tasks字段.0号进程的tasks进行遍历。tasks将所有的进程连在一块。
{
p=list_entry(pos,struct task_struct,tasks);
if(p->pid!=target_pid)
continue;
// 打印指定进程的信息
printk("进程pid: %d",p->pid);
printk("进程状态state: %d",p->__state);
printk("进程标记flags:%d",p->flags);
printk("进程调度优先级prio: %d",p->prio);
printk("进程创建时间:%lld",p->start_time);
printk("进程退出状态码:%d",p->exit_code);
printk("进程调度策略policy:%d",p->policy);
printk("进程调度静态优先级static_prio: %d",p->static_prio);
printk("父进程parent_pid: %d",(p->parent)->pid);
printk("共享表进程数: %d",atomic_read(&(p->files)->count));
printk("文件权限umask: %d",(p->fs)->umask);//linux中内核线程的mm是空的,要对它进行打印,就会出错,指针错误 if((p->mm)!=NULL) printk("Total_vm: %ld",(p->mm)->total_vm); //线性区总的页数 } printk("进程的个数:%d\n",count); return 0;
if((p->mm)!=NULL)
printk("Total_vm: %ld",(p->mm)->total_vm); //线性区总的页数
}
if (task == NULL) {
printk(KERN_INFO "找不到进程号为 %d 的进程\n", target_pid);
return -EINVAL;
}
return 0;
}
static void __exit exit_pcb(void)//出口函数
{
printk("Exiting...\n");
}
module_init(print_pcb);
module_exit(exit_pcb);
S_IRUSR:用户读权限 00400;S_IWUSR:用户写权限 00200;S_IRGRP:用户组读权限 00040;S_IROTH:其他组都权限 00004;
文件系统权限为12位,前3位表示文件拥有者的权限、中间3位表示文件所属组的权限、最后3位表示其他用户的权限,然而每个3位二进制数中我们知道第1位表示读取权限(4)、第2位表示写入权限(2)、第3位表示执行权限(1),所以我们可以将其转换成对应二进制就是110 110 110,进行位或运算转换成10进制就是664,代表文件拥有者可以读取和写入文件,但不能执行它、文件所属组的成员和其他用户只能读取文件,但不能写入或执行它。
涉及源码
pid_t pid;//进程pid
//操作系统会给每个运行的进程分配一盒唯一的整数进程PID,用来标识进程、识别进程。
unsigned int __state;//进程状态
//state状态有20种,我们选取常见的5个互为排斥的值,分别为
//ASK_RUNNING(就绪态) 0 进程处于就绪状态或者正在运行
//TASK_INTERRUPTIBLE(浅度睡眠状态) 1 进程由于中断等原因使得该进程处于挂起、阻塞进程
//TASK_UNINTERRUPTIBLE(深度睡眠状态) 2 进程处于阻塞状态,与上述不同之处在于传递一个信号和中断所引起的效果不同
//TASK_STOPPED(暂停状态) 4 进程接收到SIGSTOP、SIGTTIN、SIGTSTP或者SIGTTOU信号之后就会停止
//TASK_TRACED(跟踪状态) 8 该进程被另外的进程所监视,每一个信号都会让进程进入该状态。
//TASK_ZOMBIE(僵死状态) 32 进程已经终止但是资源已经被释放,但是父进程尚未退出,即该进程处于wait()之前的状态
观察结果图我们可以发现,没有插入模块参数之前,进程状态为0说明进程处于活跃就绪态,而当我们模块参数target_pid=221636时,进程状态转变为1说明进程处于浅睡眠状态,等待某些条件的发生并可以响应中断信号。
int exit_code;//进程退出码
//0 命令成功完成
//1通常的未知错误
//2误用shell命令
//126命令无法执行
//127没有找到命令
//128无效的退出参数
//255规范外的退出状态
观察结果图,我们也可以知道当进程处于就绪态时退出码越界即程序执行失败,而当我们插入指定模块参数时,进程退出码为0即程序正常退出。
struct fs_struct *fs;//与进程相关的文件
struct fs_struct {
int users;
spinlock_t lock;
seqcount_spinlock_t seq;
int umask;//打开文件设置文件权限时所使用的位掩码即文件权限码
int in_exec;
struct path root, pwd;
} __randomize_layout;
//fs保存一个指向文件系统信息的指针,表的地址存放于进程描述符task_struct的fs字段。该表的类型为fs_struct结构
我们发现umask(8进制数)文件权限位为1即禁止该权限位,那么它对应的二进制为001默认创建的文件将不会拥有写权限,但是会拥有执行以及读权限,我们可以扩展了解:一下umask。
位0:文件拥有执行权限
位1:文件拥有写权限
位2:文件拥有读权限
而我们通常默认系统umask=002即644权限(所有者读写、组和其他用户只读);新创建目录775权限(所有者读写执行、组和其他用户读执行)。
struct files_struct *files;//进程当前打开的文件
struct files_struct {
/*
* read mostly part
*/
atomic_t count;//共享表进程数
bool resize_in_progress;
wait_queue_head_t resize_wait;
struct fdtable __rcu *fdt;
struct fdtable fdtab;
/*
* written part on a separate cache line in SMP
*/
spinlock_t file_lock ____cacheline_aligned_in_smp;
unsigned int next_fd;
unsigned long close_on_exec_init[1];
unsigned long open_fds_init[1];
unsigned long full_fds_bits_init[1];
struct file __rcu * fd_array[NR_OPEN_DEFAULT];
};//files保存一个指向进程文件描述符表的指针,表的地址存放于进程描述符task_struct的files字段。该表的类型为files_struct结构
unsigned int policy;//进程调度策略
//linux操作系统中分为三种调度策略,分别是SCHED_OTHER分时调度即值0、SCHED_FIFO实时调度即值1、SCHED_RR实时调度即值2,优先级越高prio值越小
int prio;//进程调度优先级
int static_prio;//进程调度静态优先级
//对于实时优先级范围是0到MAX_RT_PRIO-1(即99),而普通进程的静态优先级范围是从MAX_RT_PRIO到MAX_PRIO-1(即100到139)。值越大静态优先级越低,而static_prio字段就是用来保存静态优先级,修改它可以通过nice值完成
我们policy=0说明当前进程处于分时调度策略;进程调度优先级prior=120,一般情况下我们默认调度优先级与正常优先级normal_prio的值一致;静态进程优先级static_prior=120,说明当前进程为普通进程。
u64 start_time;//进程开始时间
//u64即unsigned long long类型的数据类
struct task_struct __rcu *real_parent;//真正的父进程
/* Recipient of SIGCHLD, wait4() reports: */
struct task_struct __rcu *parent;//父进程,一般情况下与real_parent相同,如果说该进程被另外一个进程ptrace调试了,那么我们可以认为父进程就是跟踪进程
/*
* Children/sibling form the list of natural children:
*/
struct list_head children;
struct list_head sibling;
struct task_struct *group_leader;//指向线程组组长
//进程的亲属关系,我们选择打印父进程parent的p
至此,我们可以通过打印task_struct结构体字段信息以及分析内核源码对进程控制块PCB有了新的认识,我们获取打印进程的PID采用的是遍历链表,也可以可以自己尝试采用内核提供的方法find_get_pid()、get_pid_task()获取进程pid并打印该进程相关信息,后续也将继续深入学习进程相关信息。