- 论坛徽章:
- 0
|
这是我在一个论坛上偶然看到的,在讨论中有人提出LINUX系统中的fork()函数行为不够清晰。
比如在一个多线程的其中一个线程中执行fork()函数,是应该复制整个进程(即所有线程)还是仅仅复制当前调用fork()的线程。当然他对
LINUX不够熟悉。但是他提出了一个很有意思的例子来说明fork()的不足。
这里先把几个概念理一下:
多线程:由于LINUX并没有显式支持进程和线程的区别,使用了所谓的轻量级进程表示线程,所以这里的多线程表示的是共享虚拟地址空间(和信号处理所需的数据结构以及诸如文件等等,不过这里共享虚拟地址空间所引起的问题最大)。也就是通常使用fork()函数时指定CLONE_VM以及相关的flag,或者使用clone(),或者使用pthread库创建的进程。这些所有的调用最后都是使用do_fork(),外加flag的特定限制,特别的使用CLONE_VM指定新进程(线程)使用当前进程的虚拟地址空间,包括页目录和页表。
也就是说,这些多线程共用一个虚拟地址空间,包括页表和页目录。问题也就开始出现了。
现在举一个例子:假如一个SMP系统上面有若干个多线程,t1,t2,...tn,它们使用两个全局变量glob1, glob2进行某方面的同步,需要使用诸如信号量之类的锁进行互斥控制。在绝大多数情况下,都是没有问题的。现在假设线程t1执行了fork()希望创建一个进程(而不是线程,即新创建的子进程尽管初始的虚拟地址空间跟父进程完全一样,但是使用了COW技术保证它们有自己独立的虚拟地址空间)。在现在的内核中,do_fork()调用具体函数将父进程的mm_struct以及vma链表复制给子进程,同时也将父进程的页目录和页表复制给子进程的页目录和页表。但是由于内核无法完全控制父进程的页目录和页表(尽管它拥有控制互斥的自旋锁和信号量,其他CPU上内核执行路径是已经无法修改访问父进程的mm和页表,页目录。但是其他CPU上在用户态运行的线程t2,t3,..tn都可以访问页目录和页表。
假设这个多线程中有这样一个临界区代码:
...........
mutex_lock()
glob1 += 2;
glob2 -= 2;
mutex_unlock();
...........
也就是说glob1和glob2要吗都不改变,要吗都需要改变,也就是原子操作。
假设glob1和glob2属于同一个页(那么也属于同一个物理页)。当内核在执行t1的do_fork时,对于glob1和glob2所在的物理页作COW,具体一点就是把指向该物理页的pte设为只读。如果其他CPU上某个线程ti正在临界区,那么就有可能出现以下执行序列:
ti: mutex_lock()
glob1 += 2;
t1: 内核将指向该物理页的pte置为只读(COW)
ti: glob2 -= 2; //在这里,由于现在的pte是只读的,会发生缺页异常,COW将使得glob2的修改只对t1,t2,...tn可见,
//而子进程中的glob2没有被修改
mutex_unlock();
在刚创建的子进程返回到用户态时,它的glob1和glob2处在不一致状态?! |
|