标签:
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) 怎么清理僵尸进程
3、PID管理
在Linux系统中用pid结构体来标识一个进程,通过pidmap位图来管理所有的进程号(即pid:与前面的pid结构体不是同一个意思),目 的就是要更快的找到目标进程。用pid结构体来表示进程的优点:比直接用数字pid_t更容易管理(进程退出时pid回收再分配效率高),比直接用 task_struct标识进程占用空间小。
pid结构体如下所示:
因为对于32位系统来说,默认最大的pid为32768,由于pidmap位图中每一位表示这个pid是否可用,共需要32768位,正好一个物理页的大小(4*1024*8)。
pidmap结构体如下所示:
下面首先来看Linux内核启动之初在start_kernel函数中对pidmap位图的初始化函数pidmap_init如下所示:
再来看Linux内核启动之初在start_kernel函数中对pid hash表的初始化函数pidhash_init如下所示:
总结:内核维护两个数据结构来维护进程号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和内核态堆栈是在内核启动建立的,这个进程定义如下:
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) 主动切换
2) 被动切换
由于这两种情况通常发生在时钟中断或者其他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调度类和实时调度类。
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结构体释放掉。否则子进程会变成僵尸进程。
标签:
原文地址:http://www.cnblogs.com/jingzhishen/p/4433396.html