为什么此代码与单个 printf 的行为不同? ucontext.h

Why is this code acting different with a single printf? ucontext.h


当我在下面编译我的代码时,它会打印

I am running :)

永远(直到我向程序发送 KeyboardInterrupt 信号),
但是当我取消注释 // printf("done:%d\n", done);,重新编译并 运行 它时,它只会打印两次,打印 done: 1 然后 returns.
我是 ucontext.h 的新手,我对这段代码的工作方式感到非常困惑,并且 为什么单个 printf 会改变代码的整个行为,如果您将 printf 替换为 done++;,它会做同样的事情,但如果您将其替换为 done = 2;,它不会影响任何东西,并且可以作为我们首先评论了 printf
谁能解释一下:
为什么这段代码会这样,背后的逻辑是什么?
对不起,我的英语不好,
非常感谢。

#include <ucontext.h>
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>


int main()
{
    register int done = 0;
    ucontext_t one;
    ucontext_t two;
    getcontext(&one);
    printf("I am running :)\n");
    sleep(1);
    if (!done)
    {
        done = 1;  
        swapcontext(&two, &one);
    }
    // printf("done:%d\n", done);
    return 0;
}

这是一个编译器优化“问题”。当注释“printf()”时,编译器推断“done”不会在“if (!done)”之后使用,因此不会将其设置为 1,因为它不值得。但是当“printf()”存在时,“done”在“if (!done)”之后使用,所以编译器设置它。

带有“printf()”的汇编代码:

$ gcc ctx.c -o ctx -g
$ objdump -S ctx
[...]
int main(void)
{
    11e9:   f3 0f 1e fa             endbr64 
    11ed:   55                      push   %rbp
    11ee:   48 89 e5                mov    %rsp,%rbp
    11f1:   48 81 ec b0 07 00 00    sub    [=10=]x7b0,%rsp
    11f8:   64 48 8b 04 25 28 00    mov    %fs:0x28,%rax
    11ff:   00 00 
    1201:   48 89 45 f8             mov    %rax,-0x8(%rbp)
    1205:   31 c0                   xor    %eax,%eax
    register int done = 0;
    1207:   c7 85 5c f8 ff ff 00    movl   [=10=]x0,-0x7a4(%rbp) <------- done set to 0
    120e:   00 00 00 
    ucontext_t one;
    ucontext_t two;
    getcontext(&one);
    1211:   48 8d 85 60 f8 ff ff    lea    -0x7a0(%rbp),%rax
    1218:   48 89 c7                mov    %rax,%rdi
    121b:   e8 c0 fe ff ff          callq  10e0 <getcontext@plt>
    1220:   f3 0f 1e fa             endbr64 
    printf("I am running :)\n");
    1224:   48 8d 3d d9 0d 00 00    lea    0xdd9(%rip),%rdi        # 2004 <_IO_stdin_used+0x4>
    122b:   e8 70 fe ff ff          callq  10a0 <puts@plt>
    sleep(1);
    1230:   bf 01 00 00 00          mov    [=10=]x1,%edi
    1235:   e8 b6 fe ff ff          callq  10f0 <sleep@plt>
    if (!done)
    123a:   83 bd 5c f8 ff ff 00    cmpl   [=10=]x0,-0x7a4(%rbp)
    1241:   75 27                   jne    126a <main+0x81>
    {
        done = 1;  
    1243:   c7 85 5c f8 ff ff 01    movl   [=10=]x1,-0x7a4(%rbp) <----- done set to 1
    124a:   00 00 00 
        swapcontext(&two, &one);
    124d:   48 8d 95 60 f8 ff ff    lea    -0x7a0(%rbp),%rdx
    1254:   48 8d 85 30 fc ff ff    lea    -0x3d0(%rbp),%rax
    125b:   48 89 d6                mov    %rdx,%rsi
    125e:   48 89 c7                mov    %rax,%rdi
    1261:   e8 6a fe ff ff          callq  10d0 <swapcontext@plt>
    1266:   f3 0f 1e fa             endbr64 
    }
    printf("done:%d\n", done);
    126a:   8b b5 5c f8 ff ff       mov    -0x7a4(%rbp),%esi
    1270:   48 8d 3d 9d 0d 00 00    lea    0xd9d(%rip),%rdi        # 2014 <_IO_stdin_used+0x14>
    1277:   b8 00 00 00 00          mov    [=10=]x0,%eax
    127c:   e8 3f fe ff ff          callq  10c0 <printf@plt>
    return 0;

没有“printf()”的汇编代码:

$ gcc ctx.c -o ctx -g
$ objdump -S ctx
[...]
int main(void)
{
    11c9:   f3 0f 1e fa             endbr64 
    11cd:   55                      push   %rbp
    11ce:   48 89 e5                mov    %rsp,%rbp
    11d1:   48 81 ec b0 07 00 00    sub    [=11=]x7b0,%rsp
    11d8:   64 48 8b 04 25 28 00    mov    %fs:0x28,%rax
    11df:   00 00 
    11e1:   48 89 45 f8             mov    %rax,-0x8(%rbp)
    11e5:   31 c0                   xor    %eax,%eax
    register int done = 0;
    11e7:   c7 85 5c f8 ff ff 00    movl   [=11=]x0,-0x7a4(%rbp) <------ done set to 0
    11ee:   00 00 00 
    ucontext_t one;
    ucontext_t two;
    getcontext(&one);
    11f1:   48 8d 85 60 f8 ff ff    lea    -0x7a0(%rbp),%rax
    11f8:   48 89 c7                mov    %rax,%rdi
    11fb:   e8 c0 fe ff ff          callq  10c0 <getcontext@plt>
    1200:   f3 0f 1e fa             endbr64 
    printf("I am running :)\n");
    1204:   48 8d 3d f9 0d 00 00    lea    0xdf9(%rip),%rdi        # 2004 <_IO_stdin_used+0x4>
    120b:   e8 80 fe ff ff          callq  1090 <puts@plt>
    sleep(1);
    1210:   bf 01 00 00 00          mov    [=11=]x1,%edi
    1215:   e8 b6 fe ff ff          callq  10d0 <sleep@plt>
    if (!done)
    121a:   83 bd 5c f8 ff ff 00    cmpl   [=11=]x0,-0x7a4(%rbp)
    1221:   75 1d                   jne    1240 <main+0x77>
    {
        done = 1;                             <------------- done is no set here (it is optimized by the compiler)
        swapcontext(&two, &one);
    1223:   48 8d 95 60 f8 ff ff    lea    -0x7a0(%rbp),%rdx
    122a:   48 8d 85 30 fc ff ff    lea    -0x3d0(%rbp),%rax
    1231:   48 89 d6                mov    %rdx,%rsi
    1234:   48 89 c7                mov    %rax,%rdi
    1237:   e8 74 fe ff ff          callq  10b0 <swapcontext@plt>
    123c:   f3 0f 1e fa             endbr64 
    }
    //printf("done:%d\n", done);
    return 0;
    1240:   b8 00 00 00 00          mov    [=11=]x0,%eax
}
    1245:   48 8b 4d f8             mov    -0x8(%rbp),%rcx
    1249:   64 48 33 0c 25 28 00    xor    %fs:0x28,%rcx
    1250:   00 00 
    1252:   74 05                   je     1259 <main+0x90>
    1254:   e8 47 fe ff ff          callq  10a0 <__stack_chk_fail@plt>
    1259:   c9                      leaveq 
    125a:   c3                      retq   
    125b:   0f 1f 44 00 00          nopl   0x0(%rax,%rax,1)

要禁用对“完成”的优化,请在其定义中添加“volatile”关键字:

volatile register int done = 0;

这使得程序在这两种情况下都能正常工作。

(与我撰写本文时发布的 Rachid K 的回答有一些重叠。)

我猜你是在将 done 声明为 register,希望它实际上会被放入一个寄存器中,这样它的值就会被上下文切换保存和恢复。但是编译器从来没有义务遵守这一点;大多数现代编译器完全忽略 register 声明并自行决定寄存器的使用。特别是,gcc 没有优化几乎总是将内存中的局部变量放在堆栈上。

因此,在您的测试用例中,done 的值是 而不是 由上下文切换恢复的。所以当 getcontext return 第二次调用时, done 与调用 swapcontext 时的值相同。

当出现 printf 时,正如 Rachid 还指出的那样,done = 1 实际上存储在 swapcontext 之前,因此 [= 的第二个 return 15=],done 的值为 1,跳过 if 块,程序打印 done:1 并退出。

然而,当 printf 不存在时,编译器会注意到 done 的值在其赋值后从未被使用(因为它假定 swapcontext 是一个普通函数并且不我不知道它实际上会在其他地方 return),因此它优化了 dead store(是的,即使优化已关闭)。因此,当第二次 getcontext return 时,我们有 done == 0,你会得到一个无限循环。如果您认为 done 会被放置在寄存器中,这可能是您所期望的,但如果是这样,您会因为错误的原因得到“正确”的行为。

如果启用优化,您将再次看到其他内容:编译器注意到 done 不会受到对 getcontext 的调用的影响(再次假设这是一个正常的函数调用)因此它保证在 if 处为 0。所以这个测试根本不需要做,因为它永远是真的。 swapcontext然后无条件执行,至于done,它被优化得完全不存在了,因为它不再对代码有任何影响。您将再次看到无限循环。

由于这个问题,您真的无法对在 getcontextswapcontext 之间修改的局部变量做出任何安全的假设。第二次 getcontext returns 时,您可能会或可能不会看到更改。如果编译器选择围绕函数调用重新排序某些代码(它知道没有理由不这样做,因为它再次认为这些是看不到局部变量的普通函数调用),则会出现更多问题。

获得任何确定性的唯一方法是声明一个变量volatile。那么你可以确定中间的变化被看到,编译器不会假设getcontext不能改变它。在 getcontext 的第二个 return 处看到的值将与调用 swapcontext 时看到的值相同。如果你写 volatile int done = 0; 你应该只看到两条“我是 运行”消息,不管其他代码或优化设置如何。