【问题标题】:Why is this code acting different with a single printf? ucontext.h为什么此代码与单个 printf 的行为不同? ucontext.h
【发布时间】:2021-04-07 06:52:52
【问题描述】:

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

我正在跑步:)

永远(直到我向程序发送 KeyboardInterrupt 信号),
但是当我取消注释// printf("done:%d\n", done);,重新编译并运行它时,它只会打印两次,打印done: 1然后返回。
我是 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;
}

【问题讨论】:

  • 你使用什么编译器和选项?
  • @NateEldredge gcc,gcc FILE_NAME.c -o FILE_NAME.o
  • 试试gcc -O -Wall -Wextra -g FILE_NAME.c -o progname

标签: c compiler-optimization context-switching


【解决方案1】:

这是一个编译器优化“问题”。当注释“printf()”时,编译器推断在“if (!done)”之后不会使用“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    $0x7b0,%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   $0x0,-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    $0x1,%edi
    1235:   e8 b6 fe ff ff          callq  10f0 <sleep@plt>
    if (!done)
    123a:   83 bd 5c f8 ff ff 00    cmpl   $0x0,-0x7a4(%rbp)
    1241:   75 27                   jne    126a <main+0x81>
    {
        done = 1;  
    1243:   c7 85 5c f8 ff ff 01    movl   $0x1,-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    $0x0,%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    $0x7b0,%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   $0x0,-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    $0x1,%edi
    1215:   e8 b6 fe ff ff          callq  10d0 <sleep@plt>
    if (!done)
    121a:   83 bd 5c f8 ff ff 00    cmpl   $0x0,-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    $0x0,%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)

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

volatile register int done = 0;

这使得程序在这两种情况下都可以工作。

【讨论】:

  • 我认为不对,如果您将 printf 更改为 done = 2; ,它不会中断循环并且它会永远持续下去,我们也在 if 中使用 swapcontext(&amp;two, &amp;one); 更改上下文块,它不应该退出 if 块(根据我从 swapcontext 手册页获得的知识)。
  • 比较每种情况下生成的汇编代码以获得“真相”;-)
  • 按照这个逻辑,如果我们从第一个代码中删除定义中的寄存器,它应该做同样的工作(对吗?)。但事实并非如此!它打印2次并返回。我想我是从您对 Nate 回答的评论中得到的,非常感谢
【解决方案2】:

(我在写这篇文章时发布的 Rachid K 的回答有些重叠。)

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

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

printf 存在时,正如Rachid 还指出的那样,done = 1 实际上存储在swapcontext 之前,所以在getcontext 的第二次返回时,done 的值为1,@ 987654335@块被跳过,程序打印done:1并退出。

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

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

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

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

【讨论】:

  • 此外,我做了同样的测试,在“完成”的定义中添加了 volatile: volatile register int done = 0; ==> 这使得程序可以在有和没有“printf()”的情况下工作,因为编译器认为“done”可以随时更改,因此“done”没有优化
猜你喜欢
  • 1970-01-01
  • 2017-01-03
  • 2017-09-05
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-10-15
  • 2017-10-12
  • 2020-01-25
相关资源
最近更新 更多