Windows下控制子进程的句柄继承

相信用 Windows API 写过重定向子进程的标准输入输出的人都弄过这东西。大概过程是不难,不过对初学者坑很多。几年前我写了个文大概说这个的 http://blog.sorayuki.net/?p=85 ,现在要说的也是这个话题,不过是另外的事。

之前的经验是这样的,比如我有一个进程,这个进程要开个子进程、并且发数据给子进程。那么做法是创建一对匿名管道,把管道读的那一端拿去给子进程,写的这一端自己留着。容易出错的地方在这个“自己拿着”和“拿去给子进程”。拿去给子进程可以理解为,这个句柄要设置成可继承的,子进程开启以后就带着这个句柄了,那么自己进程拿着的就可以关掉(CloseHandle)了;自己拿着,那么字面意思就是不继承给子进程了。API函数CreatePipe有个 SECURITY_ATTRIBUTES 参数,这个结构体里是可以设置创建的管道句柄能不能继承的,要么两个都能继承要么两个都不能继承。但是现在要的是写的那个不继承、读的那个继承。做法不止一种,不过思路是这么两个,一个是创建的时候两个都不能继承,然后把其中一个改成可以;另一个是创建的时候两个都能继承,然后把其中一个改成不行。改继承属性可以用 SetHandleInformation 也可以用 DuplicateHandle 复制一个能/不能继承的新句柄出来、随后把原来的关掉。我现在是用 SetHandleInformation 的方法,DuplicateHandle 的方法已经忘记是在哪里看的了,现在也不知道怎么比较好坏和必要性。

复杂一点的场景,是我要起两个进程,第一个进程的输出给第二个进程的输入。麻烦一点的办法可以两个都和自己通信、自己在中间做转发;简单的办法可以直接创建一对管道,一个子进程分一个、自己不留。现在这里要说的是后者,复杂性一个表现在管道是自己创建的,但是自己在起了子进程之后一个都不留;另一个表现在要刚好一个进程继承一个,如果有一个进程继承了两个,比如写的那一端一不小心两个进程都继承了,那么很不幸这一对进程永远跑不完了:写入数据的那个子进程写完了关掉了这个写入端,但是由于读取数据的那个子进程也持有这个写端,自己有自己都不知道,也就是说这个写端没法全部被关闭,那么读的那一端就永远在等待了。

虽然上面那个例子只要搞清楚关系、小心操作的话,也没有那么大问题。不过麻烦的事情在,假设我现在是一个有好几个线程在同时执行的程序,在其中一个线程A里,要调用 CreateProcess 的时候,它不知道其他线程比如B有没有创建什么会被继承的句柄,假设这个时候线程B也在调用 CreateProcess,那么线程B创建的这个子进程会把线程A创建的那些句柄也一起继承走。然后问题呢就会出在刚才说的那个场景上,比如一对管道,读取端永远阻塞因为写入端没有全部被关闭。也许会觉得这样的场景只要在要创建子进程的时候锁个整个进程范围的锁就好了,但如果这里面调用了什么第三方库,或者是工程上好多人同时开发一个项目,有些事情就不一定有那么可控了。而且这种Bug还可能在极偶然的情况下才会遇到那么一两次,调试的时候难以重现十分痛苦……啊别说了,害怕。

继续阅读Windows下控制子进程的句柄继承