本篇主要是对《Linux内核函数之fork函数》的一个总结,主要是注明了父子进程、父子线程之间在代码中的对应关系。
fork函数是被pthread_create、fork、vfork、kernel_thread等函数复用,因此分析的时候,要注意每个函数的特点。fork所做的事情一般是先复制(直接赋值)一份current结构到当前进程p,然后再一个一个将current的工作目录、信号、信号量、信号处理函数和命名空间等复制到当前进程p(因为一直这样复制其实是比较耗时的,因此,fork采用的是COW机制,在复制了刚才说的几个重要的结构后,在复制其他非task_struct管理结构都采用的是复制它的映射表,并且会将对应的页表设置成只读,这样就可以减少复制时间),最后通过copy_thread_tls设置好新进程的寄存器和返回函数、返回值,并返回到应用空间(由于新进程是直接加入到CPU的运行队列的,所以fork返回后,到底是先运行父进程还是子进程,这个是看当时的运行情况决定)。
注:vfork保证子进程先运行,在它调用exec或exit之后父进程才能调度运行,也就是当子进程调用exec或者exit的时候, 会触发wake_up($vfork)的信号。
所谓的COW机制就是如上所说:就是只复制数据的页表,然后设置页表只读。因此,当进程对数据进行只读的时候,父子进程的内存空间依然是共享的,但是如果某个进程产生了写动作,就会因为没有写权限而产生一个do_page_fault错误,由这个do_page_falut函数重新为它分配新的内存,并建设新的映射关系,这个会在以后讲虚拟地址的时候加以说明。
注:一个内核线程只有一个栈即内核栈;一个应用进程/线程拥有两个栈即内核栈和用户栈。毫无疑问,用户栈主要用于用户态(armv7的usr模式,armv8的EL0异常),内核栈用于内核态(armv7的svc模式,armv8的EL1异常)。至于用户栈和内核栈,以后会在体系结构中加以说明。
1、子进程与父进程的关系(子进程通过task_struct->real_parent可以获取到父进程的task_struct,注意:在有调试工具介入的时候,task_struct->parent指向调试进程,否则parent和real_parent都指向父进程),如下图:
父进程可以通过task_struct->children链表找到所有子进程。子进程可以通过task_struct->real_parent找到自己所在的父进程。
2、进程与idle进程的关系,如下图:
idle进程的tasks链表上能够找到整个系统的所有进程。
3、线程与线程组的关系,如下图:
线程组组长,实际上就是一个进程,它的所有组员,都是由它用pthread_create创建的。它的所有组员都通过task_struct->group_leader找到它,同样它可以通过task_struct->thread_group遍历到所有它的组员。一个线程组中,所有线程的父进程和它的组长线程的父进程一样即task_struct->real_parent都指向同一个进程。
4、在线程共享信号中,线程的位置,如下图:
由于在Linux当中,所有同线程组下的线程其实是共享信号和信号处理函数的。什么意思呢?在Linux线程中,除了SIGSEGV(由于本身运行异常导致)、SIGBUS、SIGFPE、SIGILL之类的信号,每个线程都可能会收到这个信号(因为线程共享信号),但是只要其中一个线程处理了这个信号(可以理解为:当信号到来的时候,程序去遍历task_struct->signal->thread_node链表,去寻找可以处理信号的线程,如果找到了,就break;如果没有找到,就去查询下一个线程),其他线程就不会再收到此信号。因此,如果我们pthread_create出来的线程想要不处理这种信号,那么就需要做一定的特殊处理来屏蔽该信号(每个线程都有自己的挂起信号掩码和阻塞信号掩码)。
如上图,每个线程的signal是指向同一个的即task_struct->signal相同,可以通过遍历task_struct->signal->thread_head来得到当前线程组下的所有线程,这些线程是通过task_struct->thread_node挂接到signal->thread_head的。
注:不管是进程还是线程,都有一个不同的pid, 而同一个线程组下的所有线程,有且只有一个tgid,这个tgid的值为线程组组长的pid值。
如上图,可见:进程在fork函数中,首先通过alloc_pid函数从pid cache内存中申请一个struct pid结构(该结构会在alloc_pid中将pid添加到 pid_hash[]表中),然后把这个pid结构赋值给task_struct->pids.pid,最后又通过hlist_add_head_rcu函数将该task_struct加入到pid所指向的task链表上(这个链表分为三类:PID,PGID,SID),以后就可以通过pid来找到对应的task_struct,也可以通过task_struct来找到对应所在的pid链。
注:pid hash在Linux内核初始化的时候初始化,pidhash_init()和pidmap_init(),存在于kernel/pid.c文件中。常见查找函数:get_pid_task, find_get_pid,get_task_pid,find_task_by_vpid,find_task_by_pid_ns等。
阅读(6119) | 评论(0) | 转发(0) |