linux 进程管理剖析

发布时间:2020-09-09编辑:脚本学堂
有关linux进程的相关知识,linux进程状态,linux僵尸进程的查看与清理,linux进程切换,Linux系统的进程间通信的方式,linux进程PID管理等。

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) 怎么清理僵尸进程
1、父进程可以调用waitpid、wait函数来等待子进程结束
2、把父进程杀掉,父进程死后,僵尸进程成为“孤儿进程”,过继给init进程,init进程始终负责清理僵尸进程,它产生的所有僵尸进程也跟着消失。
 
3、PID管理
linux系统中用pid结构体来标识一个进程,通过pidmap位图来管理所有的进程号(即pid:与前面的pid结构体不是同一个意思),目的就是要更快的找到目标进程。用pid结构体来表示进程的优点:比直接用数字pid_t更容易管理(进程退出时pid回收再分配效率高),比直接用task_struct标识进程占用空间小。
pid结构体如下所示:
 

struct pid
{
atomic_t count;
int nr;  /*存放pid数值*/
struct hlist_node pid_chain;/*把该pid链到哈希表中*/
struct hlist_head tasks[PIDTYPE_MAX];
struct rcu_head rcu;
};

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

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

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

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

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

void __init pidhash_init(void)
{
int i, pidhash_size;
   /*
*nr_kernel_pages表示内核内存总页数,就是系统DMA和NORMAL内
*存页区的实际物理内存总页数
*megabytes:统计的是内核内存有多少MB
*/
unsigned long megabytes = nr_kernel_pages >> (20 - PAGE_SHIFT);
 
/*从下面两行代码可以看出pidhash_shift是在4~12之间的*/
pidhash_shift = max(4, fls(megabytes * 4));
pidhash_shift = min(12, pidhash_shift);
pidhash_size = 1 << pidhash_shift;
printk("PID hash table entries: %d (order: %d, %Zd bytes)n",
pidhash_size, pidhash_shift,
pidhash_size * sizeof(struct hlist_head));
   /*
   *由alloc_bootmem可知pid_hash是在低端物理内存申请的,由于
   *pidhash_init函数是在mem_init函数执行之前被调用的,所以这里申请
   *的内存是不会被回收的
   */
pid_hash = alloc_bootmem(pidhash_size * sizeof(*(pid_hash)));
if (!pid_hash)
panic("Could not alloc pidhash!n");
for (i = 0; i < pidhash_size; i++)
   /*初始化每个表的每个表项的链表*/
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表中增加一项。
 
4、进程的堆栈
一个进程有两个堆栈:用户态堆栈和内核态堆栈。用户态堆栈的空间指向用户地址空间,内核态堆栈的空间指向内核地址空间。
当进程由于中断或系统调用从用户态(进程在执行用户自己的代码)转换到内核态(进程在执行内核代码)时,进程所使用的栈也要从用户栈切换到内核栈。
用户栈向内核栈的切换:进入内核态后,首先把用户态的堆栈地址保存在内核堆栈中,然后设置堆栈指针寄存器的地址为内核栈地址。
内核栈向用户栈的切换:把保存在内核栈中的用户栈地址恢复到堆栈指针寄存器即可。
 
5、Linux下进程与线程的区别
l 进程是资源分配的基本单位,线程是CPU调度的基本单位
l 进程有独立的地址空间,线程有自己的堆栈和局部变量,但是没有独立的地址空间(同一个进程内的线程共享进程的地址空间)
 
6、写时拷贝机制(copy on write)
为了节约物理内存,在调用fork()生成新进程时,新进程与原进程会共享同一物理内存区(调用clone()建立线程,还会共享虚拟地址空间),只有当其中一进程进行写操作时,系统才会为其另外分配物理内存页面,这就是写时拷贝机制。

详细解释如下:当进程A使用系统调用fork()创建一个子进程B时,由于子进程B实际上是父进程A的一个拷贝,因此会拥有与父进程相同的物理页面。

为了节约内存和加快创建速度的目标,fork()函数会让子进程B以只读方式共享父进程A的物理页面,同时将父进程A对这些物理页面的访问权限也设为只读,这样,当父进程A或子进程B任何一方对这些已共享的物理页面执行写操作时,都会产生页面出错异常中断,此时CPU会执行系统提供的异常处理函数do_wp_page()来解决这个异常,do_wp_page()会对这块导致写入异常中断的物理页面取消共享操作,为写进程复制一份新的物理页面。最后,从异常处理函数返回时,CPU就会重新执行刚才导致异常的写入操作指令,使进程继续执行下去。
 
7、0号进程的建立
内核启动时“手工”建立了0号进程,即swapper进程,这是一个内核态进程,它的页表swapper_pg_dir和内核态堆栈是在内核启动建立的,这个进程定义如下:
struct task_struct init_task = INIT_TASK(init_task);

init_task的各种进程资源对象都是通过INIT_xxx进程初始化的,在start_kernel()的最后由rest_init()函数调用kernel_thread()函数,以swapper进程为“模板”建立了kernel_init内核进程,之后这个进程会建立init进程,执行/sbin//-init文件,从而把启动过程传递到用户态。

而swapper进程则执行cpu_idle()函数让出CPU,以后如果没有任何就绪的进程可调度执行,就会调度swapper进程,执行cpu_idle()函数,这个函数将调用tick_nohz_stop_sched_tick()进入tickless状态。
 
8、进程的切换
1) 主动切换
l 当前进程主动进行可能引起阻塞的I/O操作,此时当前进程被设置为等待状态,加入到相关资源的等待队列,并调用schedule()函数让出CPU。
l 进程主动通过exit系统调用退出。
2) 被动切换
l 时间片到期
l I/O中断唤醒了某个I/O等待队列中的更高优先级的进程
由于这两种情况通常发生在时钟中断或者其他I/O中断处理函数中,而中断上下文环境下不能阻塞进程,所以通常中断处理程序中通过设置need_resched标志请求调度,这个调度被延迟到中断返回处理。
 
9、Linux系统的进程间通信的方式
管道(pipe):管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用(进程的亲缘关系通常是指父子进程关系)。
命名管道(named pipe):命名管道也是半双工的通信方式,但是它允许无亲缘关系进程间的通信。

信号量(semophore):信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。

消息队列(message queue):消息队列就是一个消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。
信号(sinal):信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。

共享内存(shared memory):共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的IPC方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号配合使用,来实现进程间的同步和通信。
套接字(socket):套接字也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同主机间的进程通信。
 
10、linux进程调度机制
1) 什么是调度
从就绪的进程中选出最适合的一个来执行
2) 学习调度需要掌握哪些知识点
l 调度策略
l 调度时机
l 调度步骤
3) 调度策略
SCHED_NORMAL:普通的进程
SCHED_FIFO:先入先出的实时进程
SCHED_RR:时间片轮转的实时进程
4) 调度器类
分为CFS调度类和实时调度类。
l CFS调度类是针对普通进程的,采用的方法是完全摒弃时间片而是分配给进程一个处理器使用比重。
l 实时调度类分为SCHED_FIFO和SCHED_RR。
SCHED_FIFO实现了一种简单的、先入先出的调度算法:它不使用时间片,可以一直执行下去,只有更高优先级的SCHED_FIFO或者SCHED_RR任务才能抢占SCHED_FIFO任务。如果有两个或者更多的同优先级的SCHED_FIFO进程,它们会轮流执行,但是依然只有在它们愿意让出处理器时才会退出。
SCHED_RR与SCHED_FIFO大体相同,只是SCHED_RR级的进程在耗尽事先分配给它的时间后就不能再继续执行了。
5) 调度时机
l 主动式
在内核中直接调用schedule():当进程需要等待资源而暂时停止运行时,会把进程的状态设置为等待状态,并主动请求调度,让出CPU。
例:current->state=TASK_INTERRUPTIBLE;
schedule();
l 被动式
用户抢占:内核即将返回用户空间的时候,如果need_resched标志被设置,会导致schedule()被调用,此时就会发生用户抢占。
内核抢占
6) 调度步骤
l 清理当前运行中的进程
l 选择下一个要运行的进程
l 设置新进程的运行环境
l 进程上下文切换
 
Linux进程管理之问题
1、为什么调用fork()函数将返回两次?
这是因为在do_fork->copy_process->copy_thread函数中,将子进程的用户态堆栈的开始地址设置为父进程的用户态堆栈的开始地址,这样当父子进程从内核态返回到用户态的时候,返回的地址相同,这就解释了为什么fork一次却返回两次的原因。
 
2、为什么要在task_struct中设置mm和active_mm两个mm_struct成员呢?
这是由于内核线程没有用户态地址空间,所以它的mm设置为NULL,但是由于页目录的地址是保存在mm结构中的,从其他进程切换到这个内核态线程时,调度器可能需要切换页表,为此增加了一个active_mm,对于mm为NULL的内核态线程,就借用其他进程的mm_struct,也就是说把它的active_mm指向其他进程的mm结构,当进行进程切换时,统一使用active_mm就可以了。但是其他进程不是有自己独立的页表吗?由于内核态线程只使用内核地址空间,因此这不会有问题。
 
3、有如下说法:1.task_struct的mm成员用来描述3GB用户态虚拟地址空间;2.内核线程可以借用上一个调用的用户进程的mm中的页表来访问内核地址空间。如果是这样的话,那么task_struct的mm成员能不能描述1GB的内核地址空间?如果不能的话,为什么会有2这种说法?
task_struct的mm成员不能描述1GB的内核地址空间,只是因为mm成员中保存了页目录的信息pgd_t,而且所有进程共享1G的内核态地址空间,所以可以使用上一个用户进程的mm中的页表访问内核地址空间。
 
4、为什么所有进程共享1G的内核态地址空间?
因为fork()会复制当前进程的task_struct结构,同时会为新进程复制mm结构。此时当前进程的3GB~4GB的内核态虚拟地址对应的页表项(页目录项)被复制到进程的页表项(页目录项)中,所以说所有进程共享1G内核态地址空间。但是对于用户态虚拟地址区域,则把它的进程页表项(页目录项)设置为只读,这样当其中一个进程对其进行写入操作时,do_page_fault()会分配新的物理页面,并建立映射,从而实现COW机制。
 
5、父进程要求子进程退出时发送信号,那么父进程要求子线程退出时发送信号吗?为什么?
父进程不要求子线程退出时发送信号,这是因为子线程共享父进程的一些资源,所以不需要父进程来获取这些信息,也就不需要向父进程发送信号。这一点可以在do_fork->copy_process
 

  p->exit_signal=(clone_flags & CLONE_THREAD) ? -1 :(clone_flags &CSIGNAL);
  以及do_exit->exit_notify
  if (tsk->exit_signal != -1 && thread_group_empty(tsk)) {
  int signal = tsk->parent == tsk->real_parent ? tsk->exit_signal : SIGCHLD;
  do_notify_parent(tsk, signal);
 } else if (tsk->ptrace) {
   do_notify_parent(tsk, SIGCHLD);
 }
 

看出来。
 
6、 为什么子进程退出时,如果父进程没有调用wait等待子进程结束,则子进程会变成僵尸进程?
分析如下:在内核源码中有如下的代码:
 

do_exit->exit_notify->
 state = EXIT_ZOMBIE
 if (tsk->exit_signal == -1 &&
  (likely(tsk->ptrace == 0) ||
  unlikely(tsk->parent->signal->flags & SIGNAL_GROUP_EXIT)))
   state = EXIT_DEAD;
 tsk->exit_state = state;

说明:
如果定义了子进程退出时向父进程发送信号,则设置进程状态为EXIT_ZOMBIE,否则为EXIT_DEAD。

而子进程退出时一定会向父进程发送信号,所以进程的状态为EXIT_ZOMBIE,如果此时父进程调用wait等待子进程结束的话,由do_wait->wait_task_zombie函数可以将进程的状态设置为EXIT_DEAD,并且释放进程的内核堆栈资源,最后由put_task_struct将其task_struct结构体释放掉。

否则子进程会变成僵尸进程。