|
UNIX上C++程序设计守则(6)
准则6: 遵守多线程编程的常识
- 要准确把握在POSIX标准的函数中,那些函数是非线程安全的,一定不要使用
- 要让自己编写的函数符合线程安全
- 在访问共享数据/变量之前一定要先锁定
- 如果使用C++的话,一定要注意函数的同步方法
说明: (2) 要让自己编写的函数符合线程安全
在写多线程的应用程序时,在多个线程里共享的变量要先锁定然后在更新它.。那么在多线程里共享的变量主要有全局变量和函数内的静态变量。而且,即使是short型和int型的共享变量也要先锁定后更新才能保证其安全。
※ 详细的是参考 id:yupo5656:20040618 "[C++] 多线程和共享变量"
还有,在使用C++编程的场合要注意函数的方步方法。一般的说来下面的写法是错误的。Mutex在函数内被声明成静态变量是不允许的。
int incr_counter(void) { static Mutex m; // 这么写不行 m.Lock();
static int counter = 0; int ret = ++counter;
m.Unlock(); return ret; }
应该用下面的方式来代替,
Mutex m;
int incr_counter(void) { m.Lock(); // ...
把Mutex声明成全局变量的话比较好(稍微比上一个好)。
※ 详细是参考 id:yupo5656:20040713 "[C++] C++中写出synchronized method比较难" 。
UNIX上C++程序设计守则(6)-- 补记
线程安全函数是像下面那样
- 不要操作局部的静态变量(函数内的static型的变量)和非局部的静态数据(全局变量)。并且,其它的非线程安全函数不要调用
- 要操作这样的变量的话, 就要使用mutex进行同步处理,来限制多个线程同时对它进行操作
被定义的,但是
- 特别是前者, 和被叫做可重入的(reentrant)函数有区别
- 反之, 后者特别是和叫做"Serializable"(不单单是MT-Safe)"Safe"的函数有区别
也有以上的情况。在Solaris的man手册里, 用后者的方式进行区别. 从多线程程序里安全调用的话,就叫做"Safe", 而且, 在多线程中能够并发(concurrency)地执行这个函数的处理的话,好像就叫做"MT-Safe"。
嗯, 因为比较详细的, 如果不是在对于执行速度要求比较苛刻的环境中编写代码的话, 单单地意识到「是否线程安全」就足够了,不是吗。
UNIX上C++程序设计守则(6)
准则6: 遵守多线程编程的常识
- 要准确把握在POSIX标准的函数中,那些函数是非线程安全的,一定不要使用
- 要让自己编写的函数符合线程安全
- 在访问共享数据/变量之前一定要先锁定
- 如果使用C++的话,一定要注意函数的同步方法
说明: (1) 要准确把握那些非线程安全的函数,一定不要使用
如果在POSIX平台上进行多线程编程时,有几个最基本的知识,也就是所说的“常识”,希望大家一定要严格遵守。
...首先、我们要理解“线程安全”的意思。线程安全的函数就是指,“一个能被在多个线程同时调用也不会发生问题的函数”。这样的函数通常要满足以下几个的特质。
- 不要操作局部的静态变量(函数内的static变量)和全局静态数据(全局变量,函数外的静态变量)。而且,也不要调用其他的非线程安全的函数
- 如果要操作这样的变量的话,事先必须使用互斥锁mutex进行同步,否则一定要限制多个线程同时对它的访问
那么、在POSIX标准的函数里面,也有不满足上述条件的。由于历史遗留问题,一些函数的识别标识(signature)的定义没有考虑到线程安全的问题,所以不管怎么做都不能满足上述的条件。例如,看看 localtime函数吧。它的定义的识别标识(signature)如下:
struct tm *localtime(const time_t *timer);
localtime 函数是,把一个用整数形式表示的时刻(从1970/1/1到现在为止的秒数)、转换成一个能让人容易明白的年月日形式表示出来的tm结构体并返回给调用者的函数。根据规格说明、返回出来的tm结构体是不需要free()掉,也不能释放的。这个函数典型的实现就像下面的代码那样:
struct tm *localtime(const time_t *timer) { static struct tm t; /* ... 从timer参数里算出年月日等数值 ... */
t.tm_year = XXX; /* ...把它们填入到结构体内... */ t.tm_hour = XXX; t.tm_min = XXX; t.tm_sec = XXX;
return &t; }
这个函数如果被像下面那样使用的话,就会有漏洞:
- 在线程A里执行 ta = localtime(x);
- 在线程B里执行 tb = localtime(y);
- 线程A参照ta结构体里的数据 → 就发现这些数据是一些奇怪的值!
...在函数的说明手册里对这个问题也没有做过详细的说明。关于这个漏洞,在localtime函数即使使用了mutex锁也不能被回避掉。所以,这个函数定义的识别标识是不行滴。 [译者lymons注:在多个线程里调用localtime函数之所以有问题的原因是,localtime函数里返回的tm构造体是一个静态的结构体,所以在线程A里调用localtime函数时,该结构体被赋予正确的值;而在线程A参照这个结构体之前,线程B又调用localtime的话,这个静态的结构体又被赋予新的一个值。因此在线程A对这个结构体的访问都是基于一个错误的值进行的]
正因为如此,就像上面说过的POSIX规格(SUSv3)里整齐的定义了一些“非线程安全的函数”。在"§2.9.1 Thread-Safety" 这里登载了的非线程安全的函数有如下所示。
asctime, basename, catgets, crypt, ctime, dbm_clearerr, dbm_close, dbm_delete, dbm_error, dbm_fetch, dbm_firstkey, dbm_nextkey, dbm_open, dbm_store, dirname, dlerror, drand48, ecvt, encrypt, endgrent, endpwent, endutxent, fcvt, ftw, gcvt, getc_unlocked, getchar_unlocked, getdate, getenv, getgrent, getgrgid, getgrnam,
(省略)
对于在规格中被定义为非线程安全的函数,应该制定一个避免使用它们的规则出来,并且制作一个能够自动检查出是否使用了这些函数的开发环境,应该是比较好的。
反之,在这里没有被登载的POSIX标准函数都被假定为 "shall be thread-safe" 的、所以在实际的使用中可以认为在多线程环境里是没有问题的(而且在使用的平台上没有特别地说明它是非线程安全的话)。
另外,有几个非线程安全的函数,都准备了一个备用的线程安全版本的函数(仅仅是变更了函数的识别标识)。像这些函数为了与原版进行区别都在其函数名后面添加了 _r 这个后缀。例如,asctime函数就有线程安全版本的函数asctime_r。在规格说明中是否定义了备用函数,可以试着点击刚才的那个网页里面的函数名就可以看到。点击 rand函数就可以看到,
[TSF] int rand_r(unsigned *seed);
用[TSF]这样的文字标记出来的函数吧。这就是备用函数。在一览中没有记载出来的函数(备注: 稍微有点儿出入。请参照这里)、据我所知还有下面的备用函数。
asctime_r, ctime_r, getgrgid_r, getgrnam_r, getpwnam_r, getpwuid_r, gmtime_r, localtime_r, rand_r, readdir_r, strerror_r, strtok_r
还有,在规格以外,还准备了很多的下面那样的函数。
gethostbyname_r, gethostbyname2_r
在最近的操作系统中、也使用 getaddrinfo API函数来解决IPv6名字对应的问题。gethostbyname系列的API都是比较陈旧的函数了、所以使用前面的函数还是比较好吧。根据规格SUSv3,getaddrinfo也是线程安全的:
The freeaddrinfo() and getaddrinfo() functions shall be thread-safe.
在多线程编程中,不要使用非线程安全的函数,而他们的备用函数可以放心地积极的去使用。
→后续
[C++] UNIX上C++程序设计守则(5)
准则5: 尽可能避免线程中做延迟撤销的处理
- 线程的异步撤消是指:一个线程发出中断其他线程的处理的一个动作
- 延迟撤消因为是规格自由度比较高、所以根据OS和C库函数的版本它也有各式各样的动作
- 要想在不同的环境下都能稳定的动作的话,就必须要详细调查运行环境和,对C库函数进行抽象化,做必要的条件编译
- 在C++中、「撤消发生时的对象释放」的实现不具有可移植性
说明:
在前面我们已经讲过,线程的撤消分为「异步」「延迟」这两种类型、并且「异步撤消」也是非常容易引起各种复杂问题的元凶。
那么,现在要在程序中除掉「延迟撤消」。延迟撤消虽然不会像异步撤消那样会引起各种各样的问题、但是、注意事项还是有很多的。只有把下面的这些注意事项全部都把握之后才能放心使用。
注意事项1: 要好好把握撤消点
和异步撤消不一样的是、撤消处理一直会被延迟到在代码上明示出来的撤消点之后才会被执行。如果编写了一个具有延迟撤消可能的代码、代码中的那条语句是撤消点、必须要正确的把握。
首先、调用过pthread_testcancel函数的地方就变成撤消点了。当然这个函数是、仅仅为了「变成延迟撤消」的目的而设置出来的函数。除此之外、某些标准库函数被调用后会不会变成撤消点是在规格(SUSv3)中决定的。请参照规格说明、有下面的函数一览。
下面的函数是撤消点
accept, aio_suspend, clock_nanosleep, close, connect, creat, fcntl, fdatasync, fsync, getmsg, getpmsg, lockf, mq_receive, mq_send, mq_timedreceive, mq_timedsend, msgrcv, msgsnd, msync, nanosleep, open, pause, poll, pread, pselect, pthread_cond_timedwait, pthread_cond_wait, pthread_join, pthread_testcancel, putmsg, putpmsg, pwrite, read, readv, recv, recvfrom, (略)
下面的函数不是撤消点
access, asctime, asctime_r, catclose, catgets, catopen, closedir, closelog, ctermid, ctime, ctime_r, dbm_close, dbm_delete, dbm_fetch, dbm_nextkey, dbm_open, dbm_store, dlclose, dlopen, endgrent, endhostent, endnetent, endprotoent, endpwent, endservent, endutxent, fclose, fcntl, fflush, fgetc, fgetpos, fgets, fgetwc, fgetws, fmtmsg, fopen, fpathconf, fprintf, fputc, fputs, fputwc, fputws, (略)
看到这些我想已经明白了、但是在规格中也说明了「能否成为撤消点跟具体的实现相关的函数」也是多数存在的。原因是、为了可移植性、保证「在一定的时间内让线程的延迟撤消完成」是很困难的事情。做的不好的话、只要稍微一提升OS的版本就可能让做出来的程序产品不能动作。
即使是这样那还想要使用延迟撤消吗?
注意事项2: 实现要知道cleanup函数的必要性
可能被延迟撤销的线程在运行的过程中,要申请资源的场合,一定要考虑到以下的几点,否则就会编制出含有资源丢失和死锁的软件产品。
例如编写的下面的函数就不能被安全的延迟撤销掉。
void* cancel_unsafe(void*) { static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; pthread_mutex_lock(&mutex); // 此处不是撤消点 struct timespec ts = {3, 0}; nanosleep(&ts, 0); // 经常是撤消点 pthread_mutex_unlock(&mutex); // 此处不是撤消点 return 0; } int main(void) { pthread_t t; // pthread_create后马发上收到一个有效的延迟撤消的要求 pthread_create(&t, 0, cancel_unsafe, 0); pthread_cancel(t); pthread_join(t, 0); cancel_unsafe(0); // 发生死锁! return 0; }
在上面的样例代码中、nanosleep执行的过程中经常会触发延迟撤销的最终动作,但是这个时候的mutex锁还处于被锁定的状态。而且、线程一被延迟撤消的话就意味着没有人去释放掉这个互斥锁了。因此、在下面的main函数中调用同样的cancel_unsafe函数时就会引起死锁了。
为了回避这个问题、利用pthread_cleanup_push函数在撤消时释放掉互斥锁的话就OK了,也就不会死锁了。
// 新增清除函数 void cleanup(void* mutex) { pthread_mutex_unlock((pthread_mutex_t*)mutex); }
// 粗体字部分是新增的语句 void* cancel_unsafe(void*) { static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; pthread_cleanup_push(cleanup, &mutex); pthread_mutex_lock(&mutex); struct timespec ts = {3, 0}; nanosleep(&ts, 0); pthread_mutex_unlock(&mutex); pthread_cleanup_pop(0); return 0; }
注意事项3: 实现要清楚延迟撤消和C++之间的兼容度
使用C语言的场合,利用上面的pthread_cleanup_push/pop函数就能安全地执行延迟撤消的动作,但是在C++语言的场合就会出现其他的问题。C++与延迟撤消之间的兼容度是非常差的。具体的表现有以下两个问题:
- 执行延迟撤消的时候,内存栈上的对象的析构函数会不会被调用跟具体的开发环境有关系
- GCC3版本就不会调用。
- Solaris和Tru64 UNIX下的原生编译器的场合,就调用析构函数(好像)
- pthread_cleanup_push/pop函数和C++的异常处理机制之间有着怎样的相互影响也能具体环境有关
不调用析构函数,或者在抛出异常的时候不能做cleanup处理,经常是发生内存泄漏,资源丢失,程序崩溃,死锁等现象的原因。令人意外的是对于这个深层次的问题,就连Boost C++库都束手无策。
[Q] Why isn't thread cancellation or termination provided?
[A] There's a valid need for thread termination, so at some point Boost.Threads probably will include it, but only after we can find a truly safe (and portable) mechanism for this concept.
先必须确保对象的自由存储,而后全都让cleanup函数去释放对象的方法也有,但是这次是牺牲了异常安全性。 (原文没有看明白:オブジェクトを必ずフリーストア上に確保し、解体を全て、クリーンナップハンドラに行わせる手もありますが、今度は例外安全性が犠牲になるでしょう。)
应该说的是,在使用C++的工程里不对线程进行延迟撤消处理还是比较实际的。
原文地址:http://d.hatena.ne.jp/yupo5656/20040724/p1
■[C++] UNIX上的C++程序设计守则 (4)
铁则4: 请不要做线程的异步撤消的设计
- 线程的异步撤销是指: 某个线程的执行立刻被其他线程给强制终止了
- 请不要单单为了让“设计更简单”或者“看起了更简单”而使用线程的异步撤消
咋一看还是挺简单的。但是搞不好可能会引起各种各样的问题。请不要在不能把握问题的实质就做出使用线程的异步撤消的设计!
在pthread的规格说明中,允许一个线程可以强制中断某个线程的执行。这就是所说的异步撤消。
线程的撤消有下面的两种方式。
- 方式1: 异步撤消(PTHREAD_CANCEL_ASYNCHRONOUS)
- 方式2: 延迟撤销(PTHREAD_CANCEL_DEFERRED)
- 撤消动作,是让线程的处理一直被延迟到撤消点才会去执行
还有,到底是用哪种撤消方式,不是撤消侧,而是被撤销侧能够决定的。另外,在被撤销侧也能够选择完全禁止撤消的这种方式 。
会造成什么问题呢
那么,让我看看乱用线程的异步撤消会引起什么问题呢。看过准则3的人可能会知道,在下面的脚本里,被撤销线程以外的任意一个线程会被死锁。
- 线程1中调用malloc函数正在做内存分配的过程中,线程2异步撤消了线程1的处理
- 线程1马上被撤销,但是malloc函数中的互斥锁就没有线程去解除了
- 后面的任意一个线程如果再次调用malloc函数的话就会马上导致该线程死锁
在这个例子中使用了malloc函数,但是其他的危险函数还有很多。
反之,即使做了异步撤消也没有问题的函数也有少数存在的、我们把它们叫做「async-cancel safe函数」或者「异步撤消安全函数」。在一些商用UNIX中、OS提供的api函数的文档说明中有async-cancel safety的记载、但是在Linux(glibc)里就很遗憾,几乎没有相关的说明。
在这儿,参看规格(SUSv3)的话,会发现,、描述异步撤消安全的函数只有3个。
- pthread_cancel
- pthread_setcancelstate
- pthread_setcanceltype
而且、里面还有"No other functions are required to be async-cancel-safe"这样的记载。因此,Linux的场合,如果在文档里没有记载成async-cancel safety的函数,我们还是把它假定成不安全的函数为好!
如何避免这些问题呢
在多线程编程中为了安全的使用异步撤消处理、有没有回避死锁的方法呢?我们试着想了几个。他们与准则3里的线程+fork的场合的回避策很相似。
回避方法1: 被撤销线程中,只能使用异步撤消安全函数
首先,被撤销线程中,只能使用异步撤消安全函数。但是这个方法
- 在规格说明中只有3个异步撤消安全的函数
- 这些以外的函数是不是异步撤消安全(商用UNIX)、因为没有说明文档我们不清楚(Linux)
中有以上的两点,所以这个回避方法几乎不现实。
回避方法2: 被撤销线程中,在做非异步撤消安全处理的过程中,再把撤消方式设置成「延迟」或者是「禁止」
第二个是,被撤销线程在做非异步撤消安全处理的过程中,把撤消方式再设定成「延迟」或者「禁止」。对于这个方法
- 就像方法1写的那样、要把我那个函数是异步撤消安全的一时还是挺麻烦的
- 在任意的场所并不能保证撤消动作会被马上执行
- 例如,再设定成「延迟」后的一段时间内如果撤消发生时、某个正在阻塞的I/O函数是否能够被解除阻塞还是挺微妙的
- 如果设定成撤消禁止的话,则撤消会被屏蔽掉
有上面样的问题、会导致「一精心设计撤消方式的替换,从一开始就使用延迟撤消还不够好」这样的结果。所以这几乎是不好的一个回避策。
回避方法3: 使用pthread_cleanup_push函数,登录异步撤消时的线程数据清除的回调函数
第三种则是,用pthread_cleanup_push函数、登录一个在异步撤消发生时的数据清除的回调函数。这和在准则3中介绍的pthread_atfork函数有点儿类似。用这个函数登录的回调函数来清除线程的数据和锁,就可以回避死锁了。
...但是,pthread_cleanup_push函数登录的回调函数,在「延迟撤消」的场合是不能被调用的。因此、这个回避方法对于异步撤消没有什么大的作用。
回避方法4: 不要执行异步撤消处理
最后是、不要执行异步撤消处理。反而代之的是、
或者
- 不得不使用线程撤消的话、不做异步撤消而作延迟撤消的处理
这是比较实际的做法,是我们值得推荐的。
摘要: Linux中处理来自共享对象的同步事件
怎么利用设计模式来更有效的使用共享内存
级别:中等
Sachin Agrawal (sachin_agrawal@in.ibm.com), Senior Software Engineer, IBM Software Labs, IndiaSwati P. Udas (swatudas@in.ibm.com), Softw... 阅读全文
■[C++] UNIX上的C++程序设计守则(1)
原文:http://d.hatena.ne.jp/yupo5656/20040712/p1
Unix跟Windows等那些”对于开发者易于使用”的OS比起来,在信号和线程的利用方面有诸多的限制。但是即使不知道这些知识就做构架设计和实现的情况也随处可见。这个就是那些经常不能再现的bug的温床吧。
因此,我想分成几回来写一些准则来防止陷入到这些圈套里。
准则1:不依赖于信号收发的设计
·给其他进程以及自己发送异步信号并改变处理流程的设计不要做
- 异步信号是值用kill系统调用来创建?发送的信号、例如SIGUSR1,SIGUSR2,SIGINT,SIGTERM 等
- 简单的使用忽略信号(SIG_IGN)则没有问题
·不要把线程和信号一起使用
说明:
同步信号是指,因为某些特定的操作*1而引起向自身进程发送某些特定的信号,例如SIGSEGV,SIGBUS,SIGPIPE,SIGSYS,SIGILL,SIGFPE。异步信号就是这些以外的信号。在什么时机发送异步信号并不能被预测出来。我们会在程序里追加收到某些信号时做一些特殊处理(信号处理函数)的函数。那么根据收到的信号就跳到信号处理函数的程序就叫做”在任意代码处都能发生跳转”的程序。这样的程序往往隐藏这下面的那些问题:
- 容易引入BUG。”任意的代码”虽然也包含”执行C/C++里面的一条语句的过程中”的意思,但这很容易跳出程序员的正常思维以及默认的假定条件。编写程序的时候往往需要考虑比C++异常分支还要多得多的分支情况。
- 使测试项目激增。即使根据白盒测试达成100%的分支覆盖,也不能网罗到因为接受信号而发生的跳转分支处理。也就是说做到100%的网罗信号跳转分支的测试是不能全部实现的。一般的,加上要考虑” 在实行某个特定代码时因为接受到信号而发生的误操作”这样的BUG会经常发生*2的这种情况,测试困难往往就是导致软件的品质低下的诱因。
根据经验,”当检查到子进程结束(接收到SIGCHLD信号)时,要做必要的处理”像这样的信号处理不管做什么都是有必要的情况会有,但是除此以外的信号处理,例如
- 把自己的状态用信号告诉其他进程
- 主线程在输入输出函数里发送信号给被阻塞的子线程,并解除阻塞
等,是应该事先好好好好考虑过后再去做实际的实现。前者的话,如果不强制在”普通的”进程间进行通信的话可能会很好,后者是特意要使用线程,也要应该按照即使阻塞了也不能发生问题那样再设计。
不管怎么样,如果必须要使用信号的话,也要先全部*3理解这些陷阱以及,和多线程软件设计的场合一样或者说比它更严格的制约.注意事项都需要铭记在心里。
*1:例如,引用空指针
*2:参照 id:yupo5656:20040703 的sigsafe说明
*3:暂时先掌握”准则2”:-)
UNIX上C++程序设计守则 (2) 原文地址:http://d.hatena.ne.jp/yupo5656/20040712/p2
准则2: 要知道信号处理函数中可以做那些处理 · 在用sigaction函数登记的信号处理函数中可以做的处理是被严格限定的 · 仅仅允许做下面的三种处理 1. 局部变量的相关处理 2. “volatile sig_atomic_t”类型的全局变量的相关操作 3. 调用异步信号安全的相关函数 · 以外的其他处理不要做!
说明: 因为在收到信号时要做一些处理,那通常是准备一个信号处理函数并用sigaction函数把它和信号名进行关联的话就OK了。但是,在这个信号处理函数里可以做的处理是像上面那样被严格限定的。没有很好掌握这些知识就随便写一些代码的话就会引起下面那样的问题: · 问题1: 有程序死锁的危险 o 这是那些依赖于某一时刻,而且错误再现比较困难的BUG产生的真正原因 o 死锁是一个比较典型的例子,除此之外还能引起函数返回值不正确,以及在某一函数内执行时突然收到SEGV信号等的误操作。 ◆译者注1:SEGV通常发生在进程试图访问无效内存区域时(可能是个NULL指针,或超出进程空间之外的内存地址)。当bug原因和SEGV影响在不同时间呈现时,它们特别难于捕获到。
· 问题2: 由于编译器无意识的优化操作,有导致程序紊乱的危险 o 这是跟编译器以及编译器优化级别有关系的bug。它也是“编译器做了优化处理而不能正常动作”,“因为inline化了程序不能动作了”,“变换了OS了程序也不能动作”等这些解析困难bug产生的原因。
还是一边看具体的代码一边解说吧。在下面的代码里至少有三个问题,根据环境的不同很可能引起不正确的动作*1、按照次序来说明里面的错误。
1int gSignaled; 2void sig_handler(int signo) { 3 std::printf("signal %d received!\n", signo); 4 gSignaled = 1; 5} 6int main(void) { 7 struct sigaction sa; 8 // (省略) 9 sigaction(SIGINT, &sa, 0); 10 gSignaled = 0; 11 while(!gSignaled) { 12 //std::printf("waiting\n"); 13 struct timespec t = { 1, 0 }; nanosleep(&t, 0); 14 } 15} 16
错误1: 竞争条件 在上面的代码里有竞争条件。在sigaction函数被调用后、在gSignaled还未被赋值成0值之前,如果接受到SIGINT信号了那会变得怎么样呢? 在信号处理函数中被覆写成1后的gSignaled会在信号处理函数返回后被初始化成0、在后面的while循环里可能会变成死循环。
错误2: 全局变量gSignaled 声明的类型不正确 在信号处理函数里使用的全局变数gSignaled的类型没有声明成volatile sig_atomic_t 。这样的话、在执行while循环里的代码的时候接收到了了SIGINT信号时、有可能引起while的死循环。那为什么能引起这样的情况呢: · 信号处理函数里,把内存上gSignaled的值变更成1 ,它的汇编代码如下:
movl $1, gSignaled
· 但是,就像下面的代码描述的那样,main函数是把gSignaled的值存放到了寄存器里。在while循环之前,仅仅是做了一次拷贝变量gSignaled内存上的值到寄存器里、而在while循环里只是参照这个寄存器里的值。
movl gSignaled, %ebx .L8: testl %ebx, %ebx jne .L8
在不执行优化的情况下编译后编译器有可能不会生成上面那样的伪代码。但Gcc当使用-O2选项做优化编译时,生成的实际那样的汇编代码产生的危害并不仅仅是像上面说的威胁那样简单。这方面的问题,是设备驱动的开发者所要知道的常识,但现实情况是对于应用程序的设计者.开发者几乎都不知道这些知识。 为了解决上面的问题,全局变量gSignaled的类型要像下面那样声明。
volatile sig_atomic_t gSignaled;
volatile则是提示编译器不要像上面那样做优化处理,变成每次循环都要参照该变量内存里的值那样进行编译。所以在信号处理函数里把该变量的值修改后也能真实反映到main函数的while循环里。 sig_atomic_t 是根据CPU类型使用typedef来适当定义的整数值,例如x86平台是int类型。就是指”用一条机器指令来更新内存里的最大数据*2“。在信号处理函数里要被引用的变量必须要定义成sig_atomic_t类型。那么不是sig_atomic_t类型的变量(比如x86平台上的64位整数)、就得使用两条机器指令来完成更新动作。如果在执行一条机器指令的时候突然收到一个信号而程序执行被中断,而且在信号处理函数中一引用这个变量的话,就只能看到这个变量的部分的值。另外,由于字节对齐的问题不能由一条机器指令来完成的情况也会存在。把该变量的类型变成sig_atomic_t的话,这个变量被更新时就只需要一条机器指令就可以完成了。所以在信号处理函数里即使使用了该变量也不会出现任何问题。
2006/1/16 补充: 有一点东西忘记写了。关于sig_atomic_t详细的东西,请参考C99规范的§7.14.1.1/5小节。在信号处理函数里对volatile sig_atomic_t以外的变量进行修改,其结果都是"unspecified"的(参照译者注2)。另外, sig_atomic_t类型的变量的取值范围是在SIG_ATOMIC_MIN/MAX之间 (参见§7.18.3/2)。有无符号是跟具体的实现有关。考虑到移植性取值在0~127之间是比较合适的。C99也支持这个取值范围。C++规范(14882:2003)里也有同样的描述、确切的位置是§1.9/9这里。在SUSv3的相关描述请参考sigaction这里*3。此外、虽然在GCC的参考手册里也说了把指针类型更新成原子操作,但在标准C/C++却没有记载*4。 ◆译者注2: When the processing of the abstract machine is interrupted by receipt of a signal, the value of objects with type other than volatile sig_atomic_t are unspecified, and the value of any object not of volatile sig_atomic_t that is modified by the handler becomes undefined. ------ ISO/IEC FDIS 14882:1998(E) 的1.9小节
错误3: 在信号处理函数里调用了不可重入的函数 上述的样例代码中调用了printf函数,但是这个函数是一个不可重入函数,所以在信号处理函数里调用的话可能会引起问题。具体的是,在信号处理函数里调用printf函数的瞬间,引起程序死锁的可能性还是有的。但是,这个问题跟具体的时机有关系,所以再现起来很困难,也就成了一个很难解决的bug了。 下面讲一下bug发生的过程。首先、讲解一下printf函数的内部实现。 · printf函数内部调用malloc函数 · malloc函数会在内部维护一个静态区域来保存mutex锁、是为了在多线程调用malloc函数的时候起到互斥的作用 · 总之、malloc函数里有“mutex锁定,分配内存,mutex解锁”这样“连续的不能被中断”的处理
main関数: call printf // while循环中的printf函数 call malloc call pthread_mutex_lock(锁定malloc函数内的静态mutex) // 在malloc处理时.. ☆收到SIGINT信号! call sig_handler call printf // 信号处理函数中的printf函数 call malloc call pthread_mutex_lock(锁定malloc函数内的静态mutex) // 相同的mutex一被再度锁定,就死锁啦!!
知道上面的流程的话、像这样的由于信号中断引起的死锁就能被理解了吧。为了修正这个bug,在信号处理函数里就必须调用可重入函数。可重入函数的一览表在UNIX规范 (SUSv3)有详细记载*5。你一定会惊讶于这个表里的函数少吧。 另外,一定不要忘记以下的几点: · 虽然在SUSv3里有异步信号安全(async-signal-safe)函数的一览,但根据不同的操作系统,某些函数是没有被实现的。所以一定要参考操作系统的手册 · 第三者做成的函数,如果没有特别说明的场合,首先要假定这个函数是不可重入函数,不能随便在信 号处理函数中使用。 · 调用不可重入函数的那些函数就会变成不可重入函数了
最后,为了明确起见,想说明一下什么是” 异步信号安全(async-signal-safe)”函数。异步信号安全函数是指”在该函数内部即使因为信号而正在被中断,在其他的地方该函数再被调用了也没有任何问题”。如果函数中存在更新静态区域里的数据的情况(例如,malloc),一般情况下都是不全的异步信号函数。但是,即使使用静态数据,如果在这里这个数据时候把信号屏蔽了的话,它就会变成异步信号安全函数了。 ◆译者注3:不可重入函数就不是异步信号安全函数
*1:sigaction函数被调用前,一接收到SIGINT信号就终止程序,暂且除外吧 *2:“最大”是不完全正确的。例如,Alpha平台上32/64bit的变量用一条命令也能被更新,但是好像把8/16bit的数据更新编程了多条命令了。http://lists.sourceforge.jp/mailman/archives/anthy-dev/2005-September/002336.html 请参考这个URL地址。 *3:If the signal occurs other than as the result of calling abort(), kill(), or raise(), the behavior is undefined if the signal handler calls any function in the standard library other than one of the functions listed in the table above or refers to any object with static storage duration other than by assigning a value to a static storage duration variable of type volatile sig_atomic_t. Furthermore, if such a call fails, the value of errno is unspecified. *4:在这个手册里“ In practice, you can assume that int and other integer types no longer than int are atomic. ”这部分是不正确的。请参照Alpha的例子 *5:The following table defines a set of functions that shall be either reentrant or non-interruptible by signals and shall be async-signal-safe. 后面有异步信号安全函数一览
摘要: From 2008精选
鉄則3: マルチスレッドのプログラムでのforkはやめよう准则3:多线程程序里不准使用fork
マルチスレッドのプログラムで、「自スレッド以外のスレッドが存在している状態」でfork
何が起きるか能引起什么问题呢?
実例から見てみましょう。次のコードを実行すると、子プロセスは実行... 阅读全文
摘要: 隐锋同学的blog上有关于libxml2的一篇文章,正好最近要使用这个库来处理xml文件。
不过在测试时我们发现用文章里F. 添加属性例程代码 时,添加的keyword结点后面没有回车,
跟后面的结点挤在一行了,不是很好看。
例如,以下的xml例子文件 阅读全文
|