posted @ 2009-03-12 21:35 gussing 阅读(178) 评论(0) 编辑

1. 丘奇数

lambda演算是图灵等价的,用lambda可以模拟自然数,其中最常见的是邱奇数

    0 = λf.λx.x
1 = λf.λx.f x
2 = λf.λx.f (f x)
3 = λf.λx.f (f (f x))

简单点说,就是用函数f在x上作用了几次来表示该数字为几。λf.λx.f x作用了一次,所以该数为1;λf.λx.f (f x)作用了两次,所以该数为2,;以此类推。

在plang里,lambda的定义完全照搬上面的形式,只做两处修改:1. 参数可以有多个,包含在括号内,比如λf.λx.f x表示成\(f,x).f x 2. 作用次数明写在函数后,比如λf.λx.f (f x)表示成]\(f,x).f^2 x。\(f,x).x与\(f).\(x).x是完全等价的。

在丘奇数的基础上可以定义后继函数

SUCC = λn.λf.λx.f(n f x)

该lambda输入丘奇数n(也就是f在x上作用了n次),返回n+1(也就是再多作用一次)。在plang里表示成

var SUCC = \(n,f,x).f^(n+1) x

同样的,加法和乘法的定义是

PLUS = λm.λn.λf.λx.m f (n f x)
MULT = λm.λn.λf.m(n f)

在plang里表示成

var PLUS = \(m, n).\(f,x).f^(m+n) x
var MULT = \(m,n).\(f,x). f^(m*n) x
//或者var MULT = \(m,n).\(f,x). f^m (f^n x)

2. 谓词逻辑

lambda也可以模拟谓词逻辑,与自然数类似,该类逻辑基于丘奇布尔值之上。TRUE和FALSE的定义如下

    TRUE := λx y.x
FALSE := λx y.y

丘奇布尔值用左和右来表示真假,如果输入两个参数返回左边,则该值为真;返回右边则为假。在plang里定义如下

var TRUE = \(x,y).x
var FALSE = \(x,y).y

基于丘奇布尔我们可以定义谓词逻辑如下:

    AND := λp q.p q FALSE
OR := λp q.p TRUE q
NOT := λp.p FALSE TRUE
IFTHENELSE := λp x y.p x y

以AND为例:

AND TRUE FALSE = (λp q.p q FALSE) TRUE FALSE = TRUE FALSE FALSE = (λx y.x) FALSE FALSE = FALSE
AND TRUE TRUE = (λp q.p q FALSE) TRUE TRUE = TRUE TRUE FALSE = (λx y.x) TRUE FALSE = TRUE
AND FALSE FALSE = (λp q.p q FALSE) FALSE FALSE = FALSE FALSE FALSE = (λx y.y) FALSE FALSE = FALSE
AND FALSE TRUE = (λp q.p q FALSE) FALSE TRUE = FALSE TRUE FALSE = (λx y.y) TRUE FALSE = FALSE

非常明显,就是不断用实参代替形参产生lambda body的过程。在plang里定义如下

var TRUE = \(x,y).x;
var FALSE = \(x,y).y;
var AND = \(p,q).p q FALSE;
var OR = \(p,q).(p TRUE q);
var NOT = \(p).(p FALSE TRUE);
var IFTHENELSE = \(p,x,y).(p x y);

值得注意的是IFTHENELSE,如果p=TRUE则选择两个参数里的左边那个也就是x,如果p=FALSE则选择右边那个也就是y,正好是if的语义所在。

3. 递归和lazy eval

纯粹lambda演算的递归有些复杂,要用到不动点理论,所以我偷了点懒,在plang里除了lambda还有具名函数

defun foo(n, m)
{
return IFTHENELSE (ISZERO n) m ( foo (PREF n) (ADD m TWO) );
};

以上就是函数foo的定义,用来计算n的倍数。函数内部并非必须只有一条语句(这就是我加一个return关键字的原因),比如上述函数改写成这样也是可以的

defun foo(n, m)
{
var a = ISZERO n;
var b = PREF n;
var c = ADD m TWO;
return IFTHENELSE a m ( foo b c );
};
一个函数最终具有的值由return语句代表的值决定。如果没有return语句,则由最后一条语句决定。
眼尖的同学已经发现了这是个尾递归,可以优化成循环。plang里的计算都是lazy eval,比如foo函数,在n不为ZERO时返回的值不是最终值,而是带有 (foo b c)语句和当前环境拷贝的特殊值,等到有人要用该值(比如要
print到屏幕上,或者要被输入实参做计算时),用while循环不停的eval (foo b c)函数,以及该函数返回的结果,直到能得出实际值为止。这个过程不会让调用栈溢出,但是while循环过程中会不停的创建新环境(用来对形参做约束)。
lazy eval的另一个好处是可以短路求值,比如下面的语句
IFTHENELSE (AND TRUE FALSE) (IO print “hello") (IO print "world");
如果要把IFTHENELSE的三个参数全部求完值再返回,那么"hello" 和 "world"两个字符串势必会被全部打印出来,laze eval则只会打印正确的那个。

4. IO

本人最喜欢的语言是Haskell,Haskell作为纯函数式语言解决IO的办法是把它们包在IO Monad里,很纯很巧妙,但坦白讲难用的要死。plang里没有这么高级的货色(其实是本人水平还未够班),还是在编译器里内置了一些IO的操作。

比如在屏幕上打印的语句是

IO print "hello, world"

plang把IO函数作为一个特殊函数,也就是说,你要是自己定义了一个函数叫IO,那么编译器会抛异常。IO函数的第一个参数是io命令,目前只print和readline两个,不过以后相加的话也很容易的。一个小的echo程序如下

defun main()
{
var a = IO readline;
IO print a;
return a;
};

顺便说一句,main函数也是特殊函数,其他函数只有被调用到了才会去解释内部细节,main函数则会被自动调用到。

5. 环境

所谓的环境,就是符号和值之间的对应表,比如这段代码段

var TRUE = \(x,y).x;
var FALSE = \(x,y).y;
var AND = \(p,q).p q FALSE;
var OR = \(p,q).(p TRUE q);
var NOT = \(p).(p FALSE TRUE);
var a = AND TRUE FALSE;
var b = OR TRUE FALSE;
var c = AND (OR TRUE FALSE) FALSE;
var ZERO = \(f).\(x).f^0 x;
var ONE = \(f).\(x).f^1 x;
var N = \(f).\(x).f^n x;
var IFTHENELSE = \(p,x,y).(p x y);
defun foo(x, y)
{
var c = \(p,q).x;
return c;
};
defun main()
{
var ret = IFTHENELSE (AND TRUE FALSE) (IO print HELLO) (IO print WORLD);
var ret1 = IFTHENELSE FALSE (IO print hello1) (IFTHENELSE TRUE (IO print hello2) (IO print hello3));
IO print HELLO WORLD;
IO print TRUE;
IO print a;
var a = (foo FALSE TRUE);
var b = (a FALSE FALSE);
IO print b;
return (foo TRUE FALSE);
};

它所生产的环境如下

TRUE: ((p1,p2)->(p1))[1]
FALSE: ((p1,p2)->(p2))[1]
AND: ((p1,p2)->(p1,p2,b3))[1]
OR: ((p1,p2)->(p1,b2,p2))[1]
NOT: ((p1)->(p1,b2,b3))[1]
a: ((p1,p2)->(p2))[1]
b: ((p1,p2)->(p1))[1]
c: ((p1,p2)->(p2))[1]
ZERO: ((p1)->((p2)->(p1,p2)))[0]
ONE: ((p1)->((p2)->(p1,p2)))[1]
N: ((p1)->((p2)->(p1,p2)))[n]
IFTHENELSE: ((p1,p2,p3)->(p1,p2,p3))[1]
foo: __function__[1]
main: __function__[1]

foo和main是函数,main函数在全局环境生成后会自动被调用,foo则不会,只有main函数执行到(foo FALSE TRUE);语句时才调用。调用foo函数时会生成新的环境,在该环境里符号'x'绑定在FALSE上,符号'y'绑定在TRUE上。

返回的节点包含了自己所依赖的环境,在上述代码里,main函数里的符号a绑定在一个lambda \(p,q).x;上,而该lambda里的x符号绑定在FALSE上。每个节点都拷贝有一份自己的环境,这么做很浪费空间,但可以确保所有的自由变量都是有值的,比如下面的语句

defun foo(a)
{
var b = \(x,y).a;
return b;
};

var f1 = foo 10;
var f2 = foo 20;
IO print ( f1 1 2);
IO print ( f2 1 2);

打印的结果是

10

20

符号查找先从节点自带的环境开始,找不到则接着找全局环境,依然找不到就抛异常。

6. 实现

语法分析部分园子里的装配脑袋 有介绍,并且龙书上写的无比详细就差把伪代码翻译成真实代码了,再加上lex,yacc等东西成熟度非常高,也就没什么好说的了。目前plang的实现是解释执行的,难度比编译执行小很多。一个程序段分为很多的语句,解释器逐条解释,每解释完一条往环境里丢一个映射。遇到函数就先留一个占位的,等到真被调用到在逐句执行函数里的语句,直到碰到return或最后一个语句。当一个lambda或函数要求值时,先生成一个新的环境,然后绑定形参到实参上,在返回body的计算结果。以语句"IO print ( AND TRUE FALSE );"为例,计算的过程如下:

a. print 的参数是一个特殊节点,拥有( AND TRUE FALSE )语句,所以要循环对该节点求值

b. 查找符号“AND", 在全局环境里,是一个lambda \(p,q).p q FALSE;

c. 生成新环境e1, p约束到TRUE, q约束到FALSE,返回的节点是"TRUE FALSE FALSE"语句,该节点的环境是e1

d. 符号TRUE在全局环境中找到,是一个lambda \(x,y).x,新生成环境e2, x约束到FALSE,y约束到FALSE,返回节点FALSE,该节点的环境是e2。

e. 求值并未结束,继续查找符号"FALSE",在全局环境里找到是一个lambda,求值完成,while循环跳出

f. 打印结果。

7. 小结

这真的只是试水的东西,没怎么好好设计,代码也写的很乱。不过做一轮下来对一些以前在书里看到的知识有了直观的了解,也算是有收获吧。最后附上运行图一张:

posted @ 2012-03-25 03:16 gussing 阅读(967) 评论(4) 编辑
摘要: bash和gcc都能运行了,离“可用的系统”又进了一步。今天整理了下代码,放到了google code 上,有兴趣的都可以下载下来看。要是有谁对这也感兴趣,可以在下面留言,一起来玩。如果把讨论范围缩小到x86平台,那么linux和windows的区别,至少在用户态层面的区别,比我们想象的要小很多,所以事实上如果你真想干的话,在windows上实现各类*nix特性并没有想象中那么困难。反过来说,在*nix上实现windows特性也完全能做到。这不是随口一说,前者有cygwin, coLinux,后者有wine,都是成熟的项目。我这个东西跟cygwin的区别前面已经说过很多次了,跟coLinux的阅读全文
posted @ 2011-09-04 02:24 gussing 阅读(1170) 评论(7) 编辑

在大约一年前提到过,我想做这样一件事:打通windows上的int 80中断,让原生的linux程序也可以在windows上跑。中间因为公司项目紧,再加上idt的一个小问题困惑了我很久,所以搁置了一段时间。最近觉得周末实在闲的慌又把这项目捡起来了,并且在某次抽烟的时候突然想到“每个处理器都有自己的idt表”这一小常识,idt的问题也就很顺利的解决了。接下来的事情就变得很顺了,按照原定计划先把所有的系统调用转到用户态然后从cygwin1.dll里再过一遍,大约熬了两天夜,一个小小的demo就完成了。

代码参考了很多的开源项目,比如cygwin, glibc, linux kernel, 还有一个死了很久的项目 LINE。这事说起来也好玩,我最初想做这个项目的时候,压根没到sourceforge上去找过,包括实现原理,项目名称等都是自己想出来的。结果做了一半到sourceforge上一搜,居然有个一摸一样想法的东西早在2001年就存在了,连名字都想的一样。然后再到google上一搜,无数人都想到过类似的方案了,而且名字无一例外的打算叫成“LINE”。不得不说在开源的世界里想真正的创点新还真是困难。所幸sourceforge上的这个项目老的不成样了,是2001年的,看代码只能在win98上跑,而且是半成品,连编译都通不过,所以我做的这些事情也不是完全没价值。

不过看我这个项目仍然继承了sourceforge上这个LINE的很多东西,比如把系统调用反射回用户态这事,我原来的打算是插一个APC反射回去,看了下他们的做法,是直接把调用栈上保存的返回地址改成了用户态的一个SyscallHandler,有点类似缓冲区溢出注入的技术,这个方法毫无疑问的更合理更优雅。再比如,绝大多数的系统调用转回到用户态后只是简单的把参数包一包就转cygwin上去了,这部分代码属于纯体力劳动,我就很不客气的也照抄过来了。总之,之前我把这个项目定性为我的“创新”,现在我把它定性为LINE项目的后续开发,虽然少了重新发明轮子的快感,不过坦白我也已经过了追求这种快感的年龄了。

在开发过程中碰到的主要问题有三个:idt的问题,某些较新的linux系统调用没法实现的问题,以及路径问题。我之前设置int 80中断的时候,都是在一个IOCTL里直接就调sidt指令,以为这么做就能改掉系统idt表,实际上由于每个处理器有自己的idt表,这么干只能改IOCTL发起者所在处理器上的idt表,到了运行调试的时候一个int 80中断的发起者到底是不是在该处理器上根本没法保证,所以有很长一段时间里我就处于这么一个状况:设置好idt后一调试,好的;再调试,异常了。搞的我很莫名其妙。后来我在IOCTL处理函数里往每个处理器都插一个DPC,然后在DPC里改idt,这种时好时坏的情况就再也没发生了。

等到用户态的响应函数写的差不多,简单的linux程序比如hello world等都能跑了后,我就开始从原生linux上抓真正的程序过来试。结果一跑问题就来了:linux 2.5.29 及以后的版本新增了几个系统调用set_thread_area, get_thread_area 等等,是为了对NPTL做支持,这几个函数没办法在windows上模拟出来(至少我现在没想到办法),所以我就留空了,直接返回-ENOSYS (事实上190号以后的系统调用我全留空了)。但是现在的glibc还非用它不可,set_thread_area返回-ENOSYS后,glibc就直接打印error退出了。没办法,我只好在虚拟机上装了个redhat 6.0, 然后把那上面的程序和类库考出来用。RH 6.0 那可是史前时代的东西了,不会用到那么高级的东西,应该能跑。

最后还剩一个问题:windows的路径和*nix的路径表示完全不一致,比如linux上的程序想打开/lib/libc.so.6这个文件,在windows上根本就找不到。解决这个问题的方法倒是简单粗暴:我在open等文件操作的系统调用里做了一个小改动,凡是以‘/’打头的路径名,我就把第一个’/’去掉让他变成相对路径,然后再用windows的open函数就能顺利的打开了。直到目前为止这个小技巧还是奏效的,从RH 6.0上拷过来的基本命令都能跑,有图为证:

image

 

当然,这东西现在还只是demo而已,问题还不少。比如跑bash还是有点小问题:bash能启动,但是在bash里打ls命令又是文件未找到。再比如只能在32位机器上跑,再比如很不稳定等。总之,后续还有很多工作要做。

源代码目前还没脸放出来,等整理完后会放到google code上,沿用GPL v2。

posted @ 2011-08-25 15:43 gussing 阅读(1228) 评论(5) 编辑

上回我们留下一个未解的问题,就是当一个IRP的CancelRoutine没有被设置时,CancelIo操作会失败,系统中有可能会留下永远都不会被complete的IRP。在Threaded IRP和non-threaded IRP一节中我们有谈到irp分为线程相关和非线程相关两种。倘若一个永远不complete的irp是非线程相关的,情况会稍微好一点,顶多系统中泄露了一个资源。倘若该irp是线程相关的,那事情就大了。thread IRP由IoManager生成并保留在线程的IRP队列里,负责处理该IRP的驱动在收到下层驱动的Complete事件后不会主动收回IRP的资源而是继续complete给IoManager,由IoManager负责回收,并从线程IRP列表中删除该IRP。一个线程在退出前会遍历等待IRP队列里所有的IRP,直到它们全部被complete为止。倘若其中有一个irp永远不complete,那么线程就永远不退出,无论是ExitThread也好还是_endthreadex也好还是什么邪恶的暴力擦除数据强退也好,全都不顶用。线程不退出,进程也不能销毁(题外话:进程资源的回收动作由最后一个线程退出后发起,所谓的杀进程,其实是用apc给所有线程发起退出操作)。更糟糕的是,操作系统的关机过程都会被堵住,除了关电源,没有其他办法恢复,这一点简直比BSOD还糟糕。我们知道由user mode发起的IO操作最后都会翻译成threaded irp,这就是为什么我在7.1大谈特谈user mode线程的原因:这个陷阱连user mode程序也会掉进去。Bad dog!
要解决这一点方法很简单目标很明确,那就是防止“永远不complete的irp”这种东西出现。一般的做法是加个线程或者timer并设置超时时间,时间一到就cancel这个irp。如果irp由user mode程序发起,那么就调用CancelIo;如果irp由驱动发起,则是调用IoCancelIrp。所有这些动作要生效的大前提是你的irp有CancelRoutine的存在,否则一切都是白搭。所以这里我有个经验要跟大家分享:任何时候都给你的irp设置CancelRoutine,并在CancelRoutine里Complete你的IRP!为方便起见我们选non-threaded irp做个例子,所有的代码都在内核态,免得各位看官看示例代码还要做上下文切换。以下便是代码:

Sending thread:

IoSetCancelRoutine(Irp, MyCancelRoutine);
devext->SentIrp = Irp;

Canceling thread:

if (devext->AllocatedIrp != NULL) {
   IoCancelIrp(devext->SentIrp);
}

cancel routine里的内容都是标准步骤,不赘述。看起来已经完美无缺了,可惜拿到测试组一跑就BSOD,系统抱怨说一个irp被free了两次,肯定是有地方被疏忽了,对,我们很好的处理的例外情况,却漏掉了常规情况:irp也是可以正常complete的!假如我们的CompleteRoutine是这样的:

Completion routine:

PIRP irp;
irp = devext->SentIrp;
devext->SentIrp = NULL;
IoFreeIrp(irp);


它和CancelRoutine里用到了同一个irp,这是典型的多线程重入问题,需要加锁保护。修改后的代码如下:

Sending thread:
KeAcquireSpinLock(&devext->SentIrpLock, ...);
devext->SentIrp = Irp;
KeReleaseSpinLock(&devext->SentIrpLock, ...);


Canceling thread:
KeAcquireSpinLock(&devext->SentIrpLock, ...);
if (devext->AllocatedIrp != NULL) {
   IoCancelIrp(devext->SentIrp);
}
KeReleaseSpinLock(&devext->SentIrpLock, ...);

Completion routine:
PIRP irp;

KeAcquireSpinLock(&devext->SentIrpLock, ...);
irp = devext->SentIrp;
devext->SentIrp = NULL;
KeReleaseSpinLock(&devext->SentIrpLock, ...);

IoFreeIrp(irp);

return STATUS_MORE_PROCESSING_REQUIRED;


又是一个完美的程序,半分钟修掉一个BSOD,还有比这更爽的吗?结果一测试问题更大:系统挂起没有任何反应了。经验告诉我们这是一个死锁:Cancel thread获得spin lock后调用IoCancelIrp,IoCancelIrp最终进入CancelRoutine,而CancelRoutine则调用了IoCompleteIrp并进入Complete routine并试图再次获得spin lock,而该死的spin lock在同一条线程里也是会死锁的,这就是最终原因。
问题出在rip的完成上。设置了Cancel routine和Complete routine后,有两个点可以做irp的完成和回收动作,而这两个点只能有一个被执行。借用网上某牛的代码描述我们可以看到有以下几种情况:

// No cancellation:
//   Cancelable-->Completed
//
// Cancellation, IoCancelIrp returns before completion:
//   Cancelable --> CancelStarted --> CancelCompleted --> Completed
//
// Canceled after completion:
//   Cancelable--> Completed -> CancelStarted
//
// Cancellation, IRP completed during call to IoCancelIrp():
//   Cancelable --> CancelStarted -> Completed --> CancelCompleted


这跟同步还是两回事,同步是指两个点不能同时摸这个irp,一个摸完换另一个则是可以的,而我们要达到的目标是只要irp被其中的任何一个摸过了,另一个就不能再去摸它。为了达到这个目的,我们需要增加额外的变量记录irp被摸了几次这个信息.改造后的cancel过程如下
        if (InterlockedExchange((PVOID)&touched, IRPLOCK_CANCEL_STARTED) == IRPLOCK_CANCELABLE) {

          //
          // You got it to the IRP before it was completed. You can cancel
          // the IRP without fear of losing it, because the completion routine
          // does not let go of the IRP until you allow it.
          //
          IoCancelIrp(irp);

          //
          // Release the completion routine. If it already got there,
          // then you need to complete it yourself. Otherwise, you got
          // through IoCancelIrp before the IRP completed entirely.
          //
          if (InterlockedExchange(&touched, IRPLOCK_CANCEL_COMPLETE) == IRPLOCK_COMPLETED) {
            IoCompleteRequest(irp, IO_NO_INCREMENT);
          }
        }

而改造后的complete过程则如下
  if (InterlockedExchange((PVOID)&touched, IRPLOCK_COMPLETED) == IRPLOCK_CANCEL_STARTED) {
    //
    // Main line code has got the control of the IRP. It will
    // now take the responsibility of completing the IRP.
    // Therefore...
    IoFreeIrp(Irp);
    return STATUS_MORE_PROCESSING_REQUIRED;
  }

简单点说就是在中间加入能表示状态信息的变量touched表征现在所处的状态,cancelable, cancel started, cancel complete, completed四个状态相互协调保证complete rip不会被调用两次。如同在tip 5里提到过的,这也是口耳相传下来的范式,基本上有cancel rip的地方都得这么写。
posted @ 2011-03-28 01:09 gussing 阅读(1077) 评论(0) 编辑

虽然内核开发人员从一开始就要考虑多线程的问题,但用户态开发人员曾经有过一段美好的生活:他们只需关心一条线程(多半是UI线程)并且不必在乎太多性能问题:即使你在主逻辑里嵌套了无数层循环都没关系,该死的摩尔定律替你搞定一切问题。进入多核时代后,用户态开发人员终于发现了他们忽略很久的,但及其重要的一个技术点:多线程。朋友,好生活已经结束了,欢迎你来到混乱的时代。

我知道现在来写这篇东西似乎不合时宜,因为网上已经有无数文章讨论过多线程问题了,各个社区还开发了一个又一个的线程框架帮你解决烦人的琐事,不过我今天的主要目的是为了引出某个内核开发中的棘手问题(就是7.2要讲的,先按下不表),所以各位看官先放小弟一马,让我把旧事拉出来说说完。

说到多线程,最烦人的其实是同步问题。关于这一点我很赞同osr邮件列表里的joe老师的观点:用户态程序不应该出现(自定义的)锁,任何时候你发现自己需要考虑用锁来同步了,就说明你的设计出了问题。同步这摊子事我有一堆话要说,但不是今天,今天我要说另一个比较容易被忽略的点:线程的创建和销毁。许多人不知道如何正确的创建和销毁线程,我看到过无数错误的写法,程序奇迹般的运行正常,但错的就是错的,现在不出问题,不代表以后不会。

创建

线程创建的api是CreateThread,关于这个api只有一条原则需要注意:绝对不要去用它。让我们把时间回退到上实际70年代,那时c语言刚诞生不久,c run time library也才成型,多任务还是个高级玩意儿,如果当时就有咨询公司这种东西,他们甚至可以靠培训多任务相关技术发大财。很自然的,c运行库的作者没有考虑多线程的问题,他们假设整个c语言程序只有一条线程,没有切换,自然也没有重入,所以c运行库里有数不清的全局变量,errno就是最著名的一个。后来多任务出现了,进程和线程的概念也相继登场,这些全局变量就变得棘手了:它们会被重入。仔细推敲我们可以发现,这些全局变量其实不应该是整个地址空间可见的,而应该每个线程一份拷贝才对。实际上现在的c库就是这么干的,微软的msvcr把errno等东西放在TLS(线程本地存储块)中,创建线程的时候分配,销毁线程的时候回收。但是CreateThread作为系统api才不会管这些屁事呢,人家是系统级的,c运行库跟它没关系,问题就出在这里:你敢说你写的程序不用c运行库,所有的工作都用纯api完成?别扯了,还是听话别碰CreateThread为妙。ms vc中有替代函数_beginthreadex/_endthreadex,任何时候都必须用他们创建销毁线程。如果你用的是其他厂商的c库,就用他们提供的线程函数--不管那是什么东西,有多蠢,用就对了--别碰CreateThread。

销毁

没有哪样东西比线程的销毁更恶心人了。如同上面所说,ExitThread函数绝对不能碰,除此之外还需要注意的是:唯一正确的退出方式就是让它跑完所有代码自然退出。但很多时候自然退出根本就是一个奢望,假如你的主线程需要等所有线程退出后才能做下一件事,那么加一个超时时间就是非常必要的,因为你不能让主线程等太久,况且有些线程(特别是IO相关的线程)退不退的出还是个问题。倘若超时事件真的发生,我们就不得不做一件烂事:强制线程退出。这种做法隐患多多,我能想到的大概有以下几个:

1. 资源泄漏。假如线程开始的时候申请了内存,打开了文件,或者其他任何形式的资源,并且在自然退出前释放资源,那么_endthreadexTerminateThread后这些资源就泄漏了,没有人会去回收他们。

2. 锁的状态。假如线程开始的时候获得mutex,自然退出前释放,那么_endthreadexTerminateThread后mutex就进入ABANDONED状态,其他WaitSingleObject的点会返回WAIT_ABANDONED值。仔细想想你的代码有没有处理这个返回值,多半是没有吧…

3. IO相关的问题。如果你的GetOverlapResult调用将Wait参数置为TRUE,那么在IRP被完成之前它是不会返回的,强行退出线程会引起驱动的误会,驱动以为只要Complete了这个IRP,app就会做某类事情,实际上app没做。更糟的是如果GetOverlapResult调用将Wait参数置为FALSE并在随后的代码里进行有超时的等待,等不到就CancelIO,类似这样:

res = GetOverlapResult(…, FALSE);

if( !res )

{

  if( WAIT_TIMEOUT == WaitForSingleObject(overlap.hEvent, 5000))

  {

    CancelIo(m_hDriver);

  }

}

那么强退线程后CancelIo就有可能没被执行到,IRP可能永远都不会被Complete。没有什么比系统中存在一个永远不会complete的IRP更糟糕的事了,你的进程将永远杀不掉,系统的ShutDown过程也会被挂起,恭喜你拔电源把。

4. appverifier会直接crash进程。开发过程中一直挂着appverifier跑是个好习惯,它会把各种隐患暴露给开发人员。比如强退线程这一条,裸奔的程序还能继续执行下去,最终用户不会知道发生了什么事,但挂着appverifier的程序就会爆炸,烧掉你的硬盘,并引发9级地震。好吧我开玩笑的,但你的manager一定不会容忍crash这一点。

5. 如果调用了需要SEH才能实现的功能: raise/signal
这与TLS不同, 少了一个__try块是不能再后面补上的。
这才是比较严重的问题。
但windows下开发, 使用signal的代码确实不多。
(感谢OwnWaterloo提供)

那么到底有没有办法安全的强退线程呢,其实还是有几个的,我能想到的有以下几种:

1. 设置信号通知目标线程退出。比如定义一个BOOL exitThread值,目标线程的大循环就该写成这样

while( !exitThread)

{

  …

}

主线程则是这样:

exitThread = TRUE;

WaitForSingleObject(m_hThread, INFINITE);

这种做法绝大多数情况下有效,但是有race condition,exitThread会被重入。改成这样会更好一点

while ( WaitForSingleObject(hExitEvent,0) != WAIT_TIMEOUT )

{

  …

}

主线程

SetEvent(hExitEvent);

WaitForSingleObject(hThread, INFINITE);

但还是有问题,目标线程的逻辑里如果有调用WaitForSingleObject(…, INFINITE)无限等某事件,那么它还是退不出来。

2. 在主线程里这么干:

SuspendThread(hThread);

GetThreadContext(hThread, &THREADCONTEXT);

THREADCONTEXT.eip = my_exit;

SetThreadContext(hThread, &THREADCONTEXT);

ResumeThread(hThread);

WaitForSingleObject(hThread, INFINITE);

my_exit里释放资源,退出线程。这种做法除了IO相关问题外全都有效,写起来也很有快感,强行修改线程的执行路径,跟在内核里hook system call似的,太酷了,特黑客的感嚼。我对此的建议是:绝对不要这么做。

3. 把目标线程里的WaitForSingleObject换成WaitForSingleObejectEx,把Alertable参数设成TRUE,在主线程里这么干:

QueueUserAPC( UserAPCProc, hThread, 0 );
WaitForSingleObject( hThread, INFINITE ); 

UserAPCProc啥事也不用干,空函数就行。执行完QueueUserAPC函数后,目标线程的WaitForSingleObejectEx函数会立刻被唤醒,并返回WAIT_IO_COMPLETION状态。这是windows核心编程的作者jeffrey大牛道出的天机。这段代码写着也很有快感,居然用到APC耶,我是高手有木有!但是我对此的建议还是:绝对不要这么干。

4. 把目标线程里的WaitForSingleObject换成WaitForMultipleObjects,并在目标线程里这么写:

while ( WaitForSingleObject(hExitEvent,0) != WAIT_TIMEOUT )

{

  …

  HANDLE events[2];

  events[0] = hExitEvent;

  events[1] = hYourAnotherEventThatHaveToWait;

  if( WAIT_OBJECT_0 == WaitForMultipleObjects(2, events, FALSE, INFINITE) ) // you have to exit thread

  {

    …

  }

}

主线程则是这样:

SetEvent(hExitEvent);

WaitForSingleObject(hThread, INFINITE);

这是比较标准的做法,包括IO操作相关的一系列问题都能得到解决。个人建议如果你想让目标线程主动退出,最好采用这个手段,并且确保所有非主要线程里没有WaitForSingleObject这个函数出现。如果想等,就用WaitForMultipleObjects。

到这儿为止该说的差不多说完了,唯一的隐患就在IO那里。正确的退出要求线程从wait函数返回后执行CancelIo操作取消掉你的IRP,这需要驱动配合在IRP里设置CancelRoutine。假如没有CancelRoutine,那么CancelIo操作是失败的,前面讲的那些恐怖故事还是会发生。关于这一点,我打算下次再说。

posted @ 2011-03-27 01:24 gussing 阅读(1577) 评论(6) 编辑
摘要: 不要再假装自己写的程序没bug了,不可能的,debug工具你早晚得用上。最常见的debug工具非printf(windows上用OutputDebugString函数)莫属,简单方便易学易用,但局限性也是显而易见的,首先它对debugee的影响很大,某些race condition的bug你要多加几个log它就重现不出来了,然后你把log去了发布给客户,结果又成了必现的bug,这种烂事咱们都碰到过,你懂的。其次log能打印的东西有限,有时候你加log追某个变量的值,追到最后发现是其他变量有问题,这时候你又得加log重新跑。最后分析log的过程及其枯燥无聊,而在debug上敲命令分析则充满了乐趣阅读全文
posted @ 2011-01-24 15:12 gussing 阅读(1348) 评论(0) 编辑
摘要: IO_STACK_LOCATION很重要,再多聊一点也无妨。上上回我们谈了IO_STACK_LOCATION和那几个重要的函数,当然,我的目的不是扫盲,而是记下一些容易犯错的地方(实际上都是工作中碰到过的钉子)以方便自己回顾。我的记性是如此的差以至于几月不看就会忘记。如果你对这东西没概念,我建议你先多查查WDK文档。上回我们聊了IoCopyCurrentIrpStackLocationToNext和IoSkipCurrentIrpStackLocation的差别(你看我的记性是不是很差,其实是上上回说的),结果把要聊的核心内容给忘了。IO_STACK_LOCATION这坨东西出现的原因很大程度阅读全文
posted @ 2011-01-23 01:38 gussing 阅读(1209) 评论(2) 编辑
摘要: 今天我们聊一聊CreateFile,这个名字取的不合适但IO的世界里完全绕不过去的东西,以及与之相关的“namespace”这一概念。我们知道Create的意思是创造,创建,上帝创造了这个世界,指的可不是上帝打开了某样存在的东西(唯物主义者,我知道你们有意见,给我闭嘴…),但这个倒霉的函数要做的却是打开。我们也知道File是文件,windows里面也没有“一切都是文件”的概念,但这个倒霉的函数要做的却是打开所有能返回handle的内核对象。Anyway,CreateFile函数是唯一一个能打开内核对象的handle,并让user mode app来访问的方法。将范围缩小到驱动,这个函数也是唯一阅读全文
posted @ 2011-01-21 14:34 gussing 阅读(1617) 评论(6) 编辑
摘要: 今天我们来聊聊IRQL,这是驱动新手的梦魇,想想看多少BSOD是因为IRQL不对引起的。这也是*NIX类内核开发人员最喜欢的吐槽点之一,你看linux里就没有这个概念,我们还不是活的好好的?我偶尔有时候能得着一些空,也会问一样的问题:为毛?为毛要有这东西存在!后来我想通了。我们先聊passive level和interrupt level。passive level是普通级别,同时也是优先级最低的,所有的用户态线程和大部分的内核态线程都会在这个级别上运行。interrupt level则是中断服务例程的运行级别。这两者有差很好理解,几乎所有os教程里都有告诫我们中断服务例程要尽可能快的完成,并阅读全文
posted @ 2011-01-20 14:39 gussing 阅读(1173) 评论(0) 编辑
摘要: 如前文所述,nt内核的驱动模型没有完全使用函数调用栈,而是自己山寨出来一个IO_STACK_LOCATION,里面保存了驱动调用序列。我们知道函数调用栈的push和pop都是编译器帮忙弄的,你甚至都可以在完全不了解内幕的前提下写代码,但是驱动开发不一样,调用序列要你自己去关心,何时入栈,何时出栈,栈内保留的什么内容,全部都要照顾好,否则BSOD就在前方不远等你。与IO_STACK_LOCATION有关的函数有以下几个:IoSkipCurrentIrpStackLocation, IoSetNextIrpStackLocation, IoGetNextIrpStackLocation, IoCo阅读全文
posted @ 2011-01-19 13:54 gussing 阅读(1451) 评论(1) 编辑