Linux进程管理知识整理

时间:2022-12-31 20:01:42

Linux进程管理知识整理

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. {
  3. atomic_t count;
  4. int nr;                          /*存放pid数值*/
  5. struct hlist_node pid_chain;        /*把该pid链到哈希表中*/
  6. struct hlist_head tasks[PIDTYPE_MAX];
  7. struct rcu_head rcu;
  8. };

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

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

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

总结:内核维护两个数据结构来维护进程号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下进程与线程的区别


1)进程是资源分配的基本单位,线程是CPU调度的基本单位

2)进程有独立的地址空间,线程有自己的堆栈和局部变量,但是没有独立的地址空间(同一个进程内的线程共享进程的地址空间)

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和内核态堆栈是在内核启动建立的,这个进程定义如下:

  1. 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) 主动切换

  • 当前进程主动进行可能引起阻塞的I/O操作,此时当前进程被设置为等待状态,加入到相关资源的等待队列,并调用schedule()函数让出CPU。
  • 进程主动通过exit系统调用退出。

2) 被动切换

  • 时间片到期
  • 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) 学习调度需要掌握哪些知识点

  • 调度策略
  • 调度时机
  • 调度步骤

3) 调度策略

SCHED_NORMAL:普通的进程

SCHED_FIFO:先入先出的实时进程

SCHED_RR:时间片轮转的实时进程

4) 调度器类

分为CFS调度类和实时调度类。

  • CFS调度类是针对普通进程的,采用的方法是完全摒弃时间片而是分配给进程一个处理器使用比重。
  • 实时调度类分为SCHED_FIFO和SCHED_RR。

SCHED_FIFO实现了一种简单的、先入先出的调度算法:它不使用时间片,可以一直执行下去,只有更高优先级的SCHED_FIFO或者 SCHED_RR任务才能抢占SCHED_FIFO任务。如果有两个或者更多的同优先级的SCHED_FIFO进程,它们会轮流执行,但是依然只有在它们 愿意让出处理器时才会退出。

SCHED_RR与SCHED_FIFO大体相同,只是SCHED_RR级的进程在耗尽事先分配给它的时间后就不能再继续执行了。

5) 调度时机

  • 主动式

在内核中直接调用schedule():当进程需要等待资源而暂时停止运行时,会把进程的状态

设置为等待状态,并主动请求调度,让出CPU。

例:current->state=TASK_INTERRUPTIBLE;

schedule();

  • 被动式

用户抢占:内核即将返回用户空间的时候,如果need_resched标志被设置,会导致schedule()被调用,此时就会发生用户抢占。

内核抢占:只要重新调度是安全的,那么内核就可以在任何时间抢占正在执行的任务。

6) 调度步骤

  • 清理当前运行中的进程
  • 选择下一个要运行的进程
  • 设置新进程的运行环境
  • 进程上下文切换

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结构体释放掉。否则子进程会变成僵尸进程。