2017年9月25日 星期一

c++ RAII VS unix signal

RAII 很美好, 威力也很強大, 但一遇到 unix signal, 就沒轍了。

b.cpp
 1 #include <typeinfo>
 2 #include <iostream>
 3 #include <cstdlib>
 4 using namespace std;
 5 
 6 #include <signal.h>
 7 #include <unistd.h>
 8 
 9 int i=5;
10 void sig_handle(int sig)
11 {
12   cout << "sig: " << sig << endl;
13   i=0;
14   exit(0);
15   return;
16 }
17 
18 
19 class A
20 {
21   public:
22     A()
23     {
24       cout << "A ctor" << endl;
25     }
26     ~A()
27     {
28       cout << "~A dtor" << endl;
29     }
30 };
31 
32 
33 
34 int main(int argc, char *argv[])
35 {
36   A a;
37   signal(SIGINT, sig_handle);
38   while(1);
39   // c++ compiler 在這裡插入 A::~A()
50   return 0;
51 }

按下 Ctrl-C 之後, 解構函式不會發動, 這是很自然的, 因為 c++ 編譯器將解構函式安插在 39 那行, 而 Ctrl-C 按下後, 會執行 sig_handle, 不會在回到 39 那行, 自然無法發動解構函式。

RAII 破功? 雖然很不想承認, 但這是真的。c++ 神話破功了。

不過只要 a 是 global object, 那就沒問題, 解構函式還是會正常發動, 算是不幸中的大幸。

回到 local object 的版本, 那該怎麼辦? 如何讓 sig_handle 回到 39 那行呢?

我先是用了 exception handle, 但是失敗。

c.cpp
 1 #include <typeinfo>
 2 #include <iostream>
 3 #include <cstdlib>
 4 using namespace std;
 5 
 6 #include <signal.h>
 7 #include <unistd.h>
 8 #include <setjmp.h>
 9 
10 int i=5;
11 jmp_buf env;
12 
13 void sig_handle(int sig)
14 {
15   cout << "sig: " << sig << endl;
16   i=0;
17   throw 5;
18   return;
19 }
20 
21 
22 class A
23 {
24   public:
25     A()
26     {
27       cout << "A ctor" << endl;
28     }
29     ~A()
30     {
31       cout << "~A dtor" << endl;
32     }
33 };
34 
35 int main(int argc, char *argv[])
36 {
37   A a;
38   try
39   {
40     signal(SIGABRT, sig_handle);
41     while(1);
42   }
43   catch (...)
44   {
45     cout << "ex" << endl;
46   }
47   return 0;
48 }

ref:
Throwing an exception from within a signal handler
24.9 Using a Separate Signal Stack

我推測可能是 sig_hanlde 發動的方式和一般函式呼叫不同, sig_handle 無法回到 main, 或是回到 main 但無法正確配對到 catch 的程式碼, signal handle 連 stack 都不同於這個程式。

使用了 gdb 來驗證看看, gdb 會攔截 sigint, 我改用 sigabort, sigabort 會傳給要除錯的那支程式, 得到

gdb debug sigabort
 1 Program received signal SIGABRT, Aborted.
 2 main (argc=1, argv=0xffd83224) at c.cpp:41
 3 41      while(1);
 4 (gdb) n
 5 sig_handle (sig=6) at c.cpp:14
 6 14  {
 7 (gdb) n
 8 15    cout « "sig: " « sig « endl;
 9 (gdb) bt
10 #0  sig_handle (sig=6) at c.cpp:15
11 #1  <signal handler called>
12 #2  main (argc=1, argv=0xffd83224) at c.cpp:41

看起來好像是 main call sig_handle, 但實際上可能沒有這麼單純, 不管他了, 反正就是不能用 exception handle 處理這問題。換 exception handle 破功。

再用另外一招, 改用 setjmp/longjmp, 這就沒問題了, 解構函式正常發動。

bsjlj.cpp
 1 #include <typeinfo>
 2 #include <iostream>
 3 #include <cstdlib>
 4 using namespace std;
 5 
 6 #include <signal.h>
 7 #include <unistd.h>
 8 #include <setjmp.h>
 9 
10 int i=5;
11 jmp_buf env;
12 
13 void sig_handle(int sig)
14 {
15   cout << "sig: " << sig << endl;
16   i=0;
17   longjmp(env, 5);
18   return;
19 }
20 
21 
22 class A
23 {
24   public:
25     A()
26     {
27       cout << "A ctor" << endl;
28     }
29     ~A()
30     {
31       cout << "~A dtor" << endl;
32     }
33 };
34 
35 
36 
37 int main(int argc, char *argv[])
38 {
39   A a;
40 
41   signal(SIGINT, sig_handle);
42   if (0 == setjmp(env))
43   {
44     while(1);
45   }
46   else
47   {
48     cout << "longjmp" << endl;
49   }
50   return 0;
51 }

20170918 補充
在讀過 The Linux Programming Interface 國際中文版 21.2.1 後, 我知道為什麼要有 sigsetjmp/siglongjmp, 使用 sigsetjmp/siglongjmp 會比較正確, 但原理是一樣的, sigsetjmp/siglongjmp 只是多了處理 signal, 讓 signal 有正確的行為, 本質的流程改變的動作還是一樣的。

細節請參考 21.2.1, signal 是很複雜的東西, 我建議把 20, 21, 22 這 3 章仔細研讀過一次, 再搭配 APUE 的 signal 章節, 應該會比較認識 signal, signal 絕不是你想像中的是一個簡單的概念, 要有會付出極大心力去理解它的心裡準備。

結束了嗎? 還沒, 不要說 signal 這麼複雜的東西, exit 就可以讓 dtor 沒轍了。

r.cpp
 1 #include <typeinfo>
 2 #include <iostream>
 3 #include <cstdlib>
 4 #include <cstdio>
 5 using namespace std;
 6 
 7 #include <signal.h>
 8 #include <unistd.h>
 9 
10 class A
11 {
12   public:
13     A()
14     {
15       printf("A ctor\n");
16     }
17     ~A()
18     {
19       printf("~A dtor\n");
20     }
21 };
22 
23 int main(int argc, char *argv[])
24 {
25   A a;
26 
27   exit(1);
28   return 0;
29 }

A::~A() 不會發動呢! 那當然, 看看 r.dis, A::~A 根本沒被插到 main 裡頭, 能發動才有鬼。

r.dis (objdump -CSd r)
 1 int main(int argc, char *argv[])
 2 {
 3  8048e3c:       8d 4c 24 04             lea    0x4(%esp),%ecx
 4  8048e40:       83 e4 f0                and    $0xfffffff0,%esp
 5  8048e43:       ff 71 fc                pushl  -0x4(%ecx)
 6  8048e46:       55                      push   %ebp
 7  8048e47:       89 e5                   mov    %esp,%ebp
 8  8048e49:       51                      push   %ecx
 9  8048e4a:       83 ec 14                sub    $0x14,%esp
10   A a;
11  8048e4d:       83 ec 0c                sub    $0xc,%esp
12  8048e50:       8d 45 f7                lea    -0x9(%ebp),%eax
13  8048e53:       50                      push   %eax
14  8048e54:       e8 69 00 00 00          call   8048ec2 <A::A()>
15  8048e59:       83 c4 10                add    $0x10,%esp
16
17   exit(1);
18  8048e5c:       83 ec 0c                sub    $0xc,%esp
19  8048e5f:       6a 01                   push   $0x1
20  8048e61:       e8 fa 2a 0a 00          call   80eb960 <exit>
21
22 08048e66 <__static_initialization_and_destruction_0(int, int)>:
23   return 0;
24 }

按照慣例, 如果是 global object, 就算呼叫 exit, 依然可以正常發動 dtor。global object 是不是很好用呢?

至於為什麼 global object 可以正常發動 dtor, 不是什麼神奇的黑魔法, 在離開 main 之後, c++ runtime library 補了一段程式碼在 main 之後, 會把所有的 global/static object 的解構函式執行起來。

_exit 就不行了。

沒有留言:

張貼留言

使用 google 的 reCAPTCHA 驗證碼, 總算可以輕鬆留言了。

我實在受不了 spam 了, 又不想讓大家的眼睛花掉, 只好放棄匿名留言。這是沒辦法中的辦法了。留言的朋友需要有 google 帳號。