38

的宏展开__read_mostly

#define __read_mostly __attribute__((__section__(".data..read_mostly"))

这个来自cache.h

__init

#define __init          __section(.init.text) __cold notrace

init.h

__exit

#define __exit          __section(.exit.text) __exitused __cold notrace

通过网络搜索后,我没有找到任何关于那里发生的事情的好的解释。

附加问题:我听说过内核开发中使用的各种“链接器魔术”。任何有关此的信息都会很棒。

我对这些宏的作用有一些想法。Like__init应该表示初始化后可以删除功能代码。__read_mostly用于指示数据很少被写入,从而最大限度地减少缓存未命中。但我不知道他们是如何做到的。我的意思是它们是gcc扩展。所以理论上它们可以通过小的用户空间 c 代码来演示。

更新 1:

我试图__section__用任意部分名称来测试。测试代码:

#include <stdio.h>

#define __read_mostly __attribute__((__section__("MY_DATA")))

struct ro {
    char a;
    int b;
    char * c;
};

struct ro my_ro  __read_mostly = {
    .a = 'a',
    .b = 3,
    .c = NULL,
};


int main(int argc, char **argv) {
    printf("hello");
    printf("my ro %c %d %p \n", my_ro.a, my_ro.b, my_ro.c);
    return 0;
}

现在使用__read_mostly生成的汇编代码:

    .file   "ro.c"
.globl my_ro
    .section    MY_DATA,"aw",@progbits
    .align 16
    .type   my_ro, @object
    .size   my_ro, 16
my_ro:
    .byte   97
    .zero   3
    .long   3
    .quad   0
    .section    .rodata
.LC0:
    .string "hello"
.LC1:
    .string "my ro %c %d %p \n"
    .text
.globl main
    .type   main, @function
main:
.LFB0:
    .cfi_startproc
    pushq   %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register 6
    pushq   %rbx
    subq    $24, %rsp
    movl    %edi, -20(%rbp)
    movq    %rsi, -32(%rbp)
    movl    $.LC0, %eax
    movq    %rax, %rdi
    movl    $0, %eax
    .cfi_offset 3, -24
    call    printf
    movq    my_ro+8(%rip), %rcx
    movl    my_ro+4(%rip), %edx
    movzbl  my_ro(%rip), %eax
    movsbl  %al, %ebx
    movl    $.LC1, %eax
    movl    %ebx, %esi
    movq    %rax, %rdi
    movl    $0, %eax
    call    printf
    movl    $0, %eax
    addq    $24, %rsp
    popq    %rbx
    leave
    .cfi_def_cfa 7, 8
    ret
    .cfi_endproc
.LFE0:
    .size   main, .-main
    .ident  "GCC: (GNU) 4.4.6 20110731 (Red Hat 4.4.6-3)"
    .section    .note.GNU-stack,"",@progbits

现在没有__read_mostly宏,汇编代码或多或少保持不变。

这是差异

--- rm.S    2012-07-17 16:17:05.795771270 +0600
+++ rw.S    2012-07-17 16:19:08.633895693 +0600
@@ -1,6 +1,6 @@
    .file   "ro.c"
 .globl my_ro
-   .section    MY_DATA,"aw",@progbits
+   .data
    .align 16
    .type   my_ro, @object
    .size   my_ro, 16

所以基本上只创建了 a 小节,没什么特别的。

即使是 objdump disassmbly 也没有任何区别。

所以我对它们的最终结论是,链接器的工作是为标有特殊名称的数据部分做一些事情。我认为 linux 内核使用某种自定义链接器脚本来实现这些事情。

其中一件事是__read_mostly,可以对放置在那里的数据进行分组和管理,从而可以减少缓存未命中。

lkml有人提交了一个补丁来删除__read_mostly. 这引发了一场关于__read_mostly.

这是链接:https ://lkml.org/lkml/2007/12/13/477

我将在__init和上发布进一步的更新__exit

更新 2

这些宏__init__exit并将数据__read_mostly的内容(在 的情况下)和文本(在和的情况下)放入自定义命名的部分。这些部分由链接器使用。现在由于各种原因链接器没有用作其默认行为,因此使用链接器脚本来实现这些宏的目的。__read_mostly__init__exit

背景可以找到如何使用自定义链接器脚本来消除死代码(链接器链接到但从未执行的代码)。这个问题在嵌入式场景中非常重要。本文档讨论如何微调链接器脚本以删除死代码:elinux.org/images/2/2d/ELC2010-gc-sections_Denys_Vlasenko.pdf

如果内核可以找到初始链接描述文件include/asm-generic/vmlinux.lds.h。这不是最终脚本。这是一种起点链接描述文件针对不同的平台进行了进一步修改。

快速浏览一下这个文件,可以立即找到感兴趣的部分:

#define READ_MOSTLY_DATA(align)                     \
    . = ALIGN(align);                       \
    *(.data..read_mostly)                       \
    . = ALIGN(align);

这部分似乎正在使用“.data..readmostly”部分。

您还可以找到__init__exit部分相关的链接器命令:

#define INIT_TEXT                           \
    *(.init.text)                           \
    DEV_DISCARD(init.text)                      \
    CPU_DISCARD(init.text)                      \
    MEM_DISCARD(init.text)

#define EXIT_TEXT                           \
    *(.exit.text)                           \
    DEV_DISCARD(exit.text)                      \
    CPU_DISCARD(exit.text)                      \
    MEM_DISCARD(exit.text)

链接似乎很复杂:)

4

2 回答 2

23

GCC 属性是一种通用机制,用于向编译器提供超出语言本身规范的指令。

您列出的宏的常用功能是使用描述为的__section__属性

section属性指定函数位于特定部分中。例如,声明:

extern void foobar (void) __attribute__ ((section ("bar")));

将函数 foobar 放在 bar 部分。

那么将某些内容放在一个部分中意味着什么?一个目标文件分为几个部分:.text可执行机器代码、.data读写数据、.rodata只读数据、.bss初始化为零的数据等。这些部分的名称和用途是平台约定的问题,还有一些特殊的只能使用__attribute__ ((section))语法从 C 中访问节。

在您的示例中,您可以猜测这是大部分将被读取的数据.data..read_mostly的子部分;是一个文本(机器代码)部分,将在程序初始化等时运行。.data.init.text

在 Linux 上,决定如何处理各个部分是内核的工作;当用户空间请求exec程序时,它将逐段读取程序映像并适当地处理它们:.data段被映射为读写页面、.rodata只读页面、只.text执行页面等。大概.init.text会在程序开始;这可以由内核或放置在程序入口点的用户空间代码来完成(我猜是后者)。

如果您想查看这些属性的效果,一个好的测试是运行 gcc 并-S选择输出汇编代码,其中将包含节指令。然后,您可以使用或不使用节指令运行汇编程序,并使用objdump甚至十六进制转储生成的目标文件以查看它的不同之处。

于 2012-07-16T15:23:11.430 回答
18

据我所知,这些宏仅供内核使用。从理论上讲,它们可以应用于user-space,但我认为情况并非如此。它们都相似的变量和代码组合在一起以获得不同的效果。

初始化/退出

设置内核需要大量代码;这发生在任何用户空间运行之前。即,在init 任务运行之前。在许多情况下,此代码永远不会再次使用。因此,在启动后消耗不可交换的RAM 将是一种浪费。熟悉的内核消息Freeing init memory是该init部分的结果。一些驱动程序可能配置为模块。在这些情况下,他们退出。但是,如果它们被编译到内核中,则不一定会退出(它们可能会关闭)。这是对此类代码/数据进行分组的另一部分。

冷热

每个高速缓存行具有固定大小。您可以通过将相同类型的数据/函数放入其中来最大化缓存。这个想法是经常使用的代码可以并排使用。如果高速缓存是四条指令,则一个热例程的结束应该与下一个热例程的开始合并。同样,将很少使用的代码放在一起也很好,因为我们希望它永远不会进入缓存

主要阅读

这里的想法类似于hot;与数据的差异我们可以更新值。完成后,整个高速缓存行变,必须重新写入主 RAM。这对于多 CPU 一致性以及该缓存行过时是必需的。如果 CPU缓存版本和主内存之间的差异没有任何变化,那么eviction不需要发生任何事情。这优化了 RAM 总线,以便其他重要的事情可以发生。

这些项目严格用于内核。可以(是?)为用户空间实施类似的技巧。这取决于使用的装载机;这通常取决于使用的libc

于 2013-10-07T19:50:13.187 回答