Linux进程管理知识整理(1)


1、进程有哪些状态?什么是进程的可中断等待状态?进程退出后为什么要等待调度器删除其task_struct结构?进程的退出状态有哪些?


TASK_RUNNING可运行状态)

TASK_INTERRUPTIBLE可中断等待状态)

TASK_UNINTERRUPTIBLE不可中断等待状态)

TASK_STOPPED进程被其它进程设置为暂停状态)

TASK_TRACED进程被调试器设置为暂停状态)

TASK_DEAD退出状态)

进程由于所需资源得不到满足,从而进入等待队列,但是该状态能够被信号中断。比如当一个正在运行的进程因进行磁盘I/O操作而进入可中断等待状态时,在I/O操作完成之前,用户可以向该进程发送SIGKILL,从而使该进程提前结束等待状态,进入可运行态,以便响应SIGKILL,执行进程退出代码,从而结束该进程。

当进程退出时例如调用exit或者从main函数返回),需要向父进程发送信号,父进程进行信号处理时,需要获取子进程的信息,因此这时不能删除子进程的task_struct。另外每个进程都有一个内核态的堆栈,当进程调用exit()时,在切换到另外一个进程前,总是要使用内核态堆栈,因此当进程调用exit()时,完成必要的处理后,就把state设置为TASK_DEAD,并切换到其他进程。当顺利地切换到其他进程后,由于该进程的状态设置为TASK_DEAD,因此这个进程不会被调度,之后当调度器检查到状态为TASK_DEAD的进程时,就会删除这个进程的task_struct结构,这样这个进程就彻底的消失了。

EXIT_ZOMBIE僵死进程):父进程等待子进程结束时发送的SIGCHLD信号默认情况下,创建进程都会设置在进程退出的时候向父进程发送信号的标志,除非创建的是轻权进程),此时子进程已退出,并且SIGCHLD信号已经发送,但是父进程还没有被调度运行;EXIT_DEAD僵死撤销状态):父进程对子进程的退出信号“没兴趣”,或者在子进程退出时,父进程通过waitpid()调用等待子进程的SIGCHLD信号。

2、僵尸进程


1) 怎么产生僵尸进程

一个进程在调用exit命令结束自己的时候,其实它并没有真正的被销毁,只是进程不能被调度并处于EXIT_ZOMBIE状态,它占用的所有内存就是内核栈、thread_info结构和task_struct结构。此时进程存在的唯一目的就是向它的父进程提供信息,如果它的父进程没有调用wait或waitpid等待子进程结束,又没有显示地忽略该信号,那么它就一直保持EXIT_ZOMBIE状态。

2) 怎么查看僵尸进程

利用命令ps,看到有标记为Z的进程就是僵尸进程。

3) 怎么清理僵尸进程

  • 父进程可以调用waitpid、wait函数来等待子进程结束
  • 把父进程杀掉,父进程死后,僵尸进程成为“孤儿进程”,过继给init进程,init进程始终负责清理僵尸进程,它产生的所有僵尸进程也跟着消失。

3、PID管理


在Linux系统中用pid结构体来标识一个进程,通过pidmap位图来管理所有的进程号(即pid:与前面的pid结构体不是同一个意思),目的就是要更快的找到目标进程。用pid结构体来表示进程的优点:比直接用数字pid_t更容易管理(进程退出时pid回收再分配效率高),比直接用task_struct标识进程占用空间小。

pid结构体如下所示:

  1. struct pid 
  2. atomic_t count; 
  3. int nr;                          /*存放pid数值*/ 
  4. struct hlist_node pid_chain;        /*把该pid链到哈希表中*/ 
  5. struct hlist_head tasks[PIDTYPE_MAX]; 
  6. struct rcu_head rcu; 
  7. }; 

因为对于32位系统来说,默认最大的pid为32768,由于pidmap位图中每一位表示这个pid是否可用,共需要32768位,正好一个物理页的大小(4*1024*8)。

pidmap结构体如下所示:

  1. struct pidmap { 
  2.        /* 
  3.         *这个变量用来统计这个结构体对应的一页物理内存中有多少个位 
  4.             *是0的,即空闲pid的数量 
  5.         */ 
  6.        atomic_t nr_free;  
  7.        void *page;      /*这个就是指向存放这个位图的内存页的指针*/ 
  8. }; 

下面首先来看Linux内核启动之初在start_kernel函数中对pidmap位图的初始化函数pidmap_init如下所示:

  1. void __init pidmap_init(void) 
  2.    /*申请一页物理内存,并初始化为0*/ 
  3. init_pid_ns.pidmap[0].page = kzalloc(PAGE_SIZE, GFP_KERNEL); 
  4.   /*将第0位设置为1,表示当前进程使用pid为0,即现在就是0号进程*/ 
  5. set_bit(0, init_pid_ns.pidmap[0].page); 
  6.    /*同时更新nr_free统计空闲pid的值*/ 
  7. atomic_dec(&init_pid_ns.pidmap[0].nr_free); 
  8. pid_cachep = KMEM_CACHE(pid, SLAB_PANIC); 

再来看Linux内核启动之初在start_kernel函数中对pid hash表的初始化函数pidhash_init如下所示:

  1. void __init pidhash_init(void) 
  2. int i, pidhash_size; 
  3.    /* 
  4.     *nr_kernel_pages表示内核内存总页数,就是系统DMA和NORMAL内 
  5.         *存页区的实际物理内存总页数 
  6.     *megabytes:统计的是内核内存有多少MB 
  7.     */ 
  8. unsigned long megabytes = nr_kernel_pages >> (20 - PAGE_SHIFT); 
  9.   /*从下面两行代码可以看出pidhash_shift是在4~12之间的*/ 
  10. pidhash_shift = max(4, fls(megabytes * 4)); 
  11. pidhash_shift = min(12, pidhash_shift); 
  12. pidhash_size = 1 << pidhash_shift
  13. printk("PID hash table entries: %d (order: %d, %Zd bytes)\n", 
  14. pidhash_size, pidhash_shift, 
  15. pidhash_size * sizeof(struct hlist_head)); 
  16.    /* 
  17.    *由alloc_bootmem可知pid_hash是在低端物理内存申请的,由于 
  18.        *pidhash_init函数是在mem_init函数执行之前被调用的,所以这里申请  
  19.        *的内存是不会被回收的 
  20.    */ 
  21. pid_hash = alloc_bootmem(pidhash_size * sizeof(*(pid_hash))); 
  22. if (!pid_hash) 
  23. panic("Could not alloc pidhash!\n"); 
  24. for (i = 0; i < pidhash_size; i++) 
  25.        /*初始化每个表的每个表项的链表*/ 
  26. INIT_HLIST_HEAD(&pid_hash[i]); 

总结:内核维护两个数据结构来维护进程号pid,一个是哈希表pid_hash,还有一个位图pidmap。在do_fork()中每调用一次alloc_pid(),首先会通过调用alloc_pidmap()修改相应的位图,该函数的主要思想是:last记录上次分配的pid,此次分配的pid为last+1,如果pid超出最大值,那么就循环回到最初值(RESERVED_PIDS),然后测试pidmap上该pid所对应的bit是否为0,直到找到为止。其次通过hlist_add_head_rcu函数在pid_hash表中增加一项。


相关内容