GNU C语言扩展
1. 语句表达式
在GNU C语言中, 括号里的复合语句可以看作一个表达式,称为语句表达式.在语句表达式里,可以使用循环,跳转和局部变量等,这个特性通常用在宏定义中,可以让宏定义变得更安全,如比较两个值的大小:
青铜用法:
#define max(a, b) a > b ? a : b
这种用法的缺陷很多,愿称之为初学者的水平.
反例:
#include <stdio.h>
#define max(a, b) a > b ? a : b
int main()
{
int i = 4, y = 2, k = 3;
i = i + max(y, k);
printf("%d\n", i);
}
期望输出 : 7
实际输出 : 2
黄金用法:
#define max(a, b) ((a) > (b) ? (a) : (b))
这种做法把宏作为一个整体,避免了"青铜"用法出现的错误(可以去试一下),但如果我掏出这段代码又该如何应对呢?
#include <stdio.h>
#define max(a, b) ((a) > (b) ? (a) : (b))
int main()
{
int i = 4, y = 2;
max(y++, i++);
printf("y = %d i = %d\n", y, i);
}
期望输出: y = 3 i = 5
实际输出: y = 3 i = 6
明显可知, i++ 执行了两次.请自行思考,不会可以在评论区里讨论.
钻石用法:
#define max(a, b) \
({ \
int _a = (a); \
int _b = (b); \
_a > _b ? _a : _b; \
})
这种方法无论 a 大还是 b 大, 黄金用法的反例都只会自增一次.但是这个做法有个很明显的缺陷,只能比较 int 类型的两个数,如果要比较两个浮点类型的话还得重新写一个宏,或者比较的两个数不是同一类型的变量时也只是机械性地将它们转换为 int 类型, 说白了就是没有类型检查.
王者用法:
钻石用法的前提是知道两个数的类型,如果不知道两个数的类型,还可以使用 typeof 类转换宏.
#define max(a, b) \
({ \
typeof(a) _a = a; \
typeof(b) _b = b; \
(void) (&_a == &_b); \
_a > _b ? _a : _b; \
})
这里使用了GNU C语言的一个扩充用法 typeof ,可以用来构造新的类型,通常和语句表达式一起使用.说白点就是把类型也设成一个参数.
(void) (&_a == &_b);
这个语句是为了在编译时检查 `_a` 和 `_b` 是否具有相同的地址。它使用了 `&` 运算符获取 `_a` 和 `_b` 的地址,并比较这两个地址是否相同。这里使用 `(void)` 是为了避免出现“unused value”警告.
这个宏定义的思路是,通过定义临时变量 `_a` 和 `_b`,以及检查它们的地址,来确保它们具有相同的类型,然后通过条件运算符判断返回最大值。这样,使用这个宏定义时,就能够方便地求出两个值的最大值了。
难以理解 typeof ?
举例:
double i, *a = i;
typeof (*a) b;
typeof (*a) c[5];
typeof (typeof (char *)[6]) m;
第一句声明了一个double变量 i ,和指向 i 的指针 a.
第二句声明了一个变量 b ,其类型是 指针 a 所指向的变量类型,即 b 的类型为 i 的类型,即 : double.
第三句声明了一个数组 c ,其大小为5,而这个 5 的单位就是 double , 即 c 是一个有5个double类型元素的数组.
第四句声明了一个指针数组 m , 与 char* m[6] 的效果是一样的.
2.零长数组
先来看看两个在通信时常用的数据包的数据结构.
定长包
struct send_buffer
{
int len;
unsigned char data[max_len];
}
这两个是常用于两个进程通信时用的结构体,其中 "len" 是数据包大小,而 "data" 则是数据.
第一种是用数组保存将要发送的数据,但是得提前预算好数据的最大大小,而且有个弊端,当我某次发送的数据长度只有512个字节时,而提前设定好的 max_len 是1024,那么就会产生空间和通信流量的浪费.因为要考虑到数据的溢出, 变长数据包中的 data 数组长度一般会设置得足够长足以容纳最大的数据.
考虑到字节对齐,在使用这种数据结构时需要提前申请临时空间, 以 max_len 为1024为例.每次都要申请 1024 + 4 = 1028 个字节.
//申请
struct send_buffer* buf = NULL;
if ((buf = (struct send_buffer *)malloc(sizeof(struct send_buffer))) != NULL)
{
buf->len = data_len;
memcpy(buf->data, "Hello World", data_len);
printf("%d, %s\n", buf->len, buf->data);
}
//使用
...
...
...
//释放
free(buf);
buf = NULL;
1.优点:数据的开辟和释放操作简单.
2.缺点:因为要提前设好最大数据大小所以使用时经常会造成空间和传输流量的浪费.
指针包
struct send_buffer
{
int len;
unsigned char* data; //可变数组
};
这种和第一种差不多,但是数据则是由一个指针所指向,在使用该数据结构时只需要多一个指针便能避免定长包的数据空间浪费,因为此时要发送的数据当是提前准备好时只需将这个指针指向就行,当是即时生成数据时,只需开辟 "len" 个大小的空间,然后将这个指针指向这片内存就可以.
//申请
struct send_buffer* buf = NULL;
if ((buf = (struct send_buffer *)malloc(sizeof(struct send_buffer))) != NULL)
{
buf->len = data_len;
if ((buf->data = (char *)malloc(sizeof(char) * data_len)) != NULL)
{
memcpy(buf ->data, "Hello World", data_len);
printf("%d, %s\n", buf->len, buf->data);
}
//或者直接指向已经生成好的数据包变量
}
//使用
...
...
...
//释放
free(buf->data);
free(buf);
buf = NULL;
1.优点:只使用了一个指针就避免了第一种包数据结构的缺点,不会造成空间浪费.
2.缺点:使用时操作可能不太方便,因为多次申请了内存空间,使用不当可能会造成内存泄漏.
零长数组包
GNU/GCC 在标准的 C/C++ 基础上做了有实用性的扩展, 零长度数组就是其中一个知名的扩展.
即变长数组/可变数组/柔性数组,在定义数据结构时非常有用.
<mm/percpu.c>
struct pcpu_chunk
{
struct list_head list;
unsigned long populated[0]; //可变数组
};
数据结构最后一个元素被定义为零长度数组,不占用结构体空间,这样就可以根据对象大小动态分配结构的大小 ,对于编译器而言, 此时长度为0的数组并不占用空间, 因为数组名本身不占空间, 它只是一个偏移量, 数组名这个符号本身代表了一个不可修改的地址常量.
struct send_buffer
{
int len;
char data[0];
};
在这个结构体里,虽然有两个成员,但是大小只包含了一个 int 类型的大小,即 sizeof(struct send_buffer) = sizeof(int).
创建结构体对象时,可根据需求来指定这个可变长数组的长度,并分配相应大小的空间,并且可以通过下标来访问数组的元素.
申请时只需提前知道想要发送数据的大小即可.而释放只需要释放结构体变量即可.
//申请
struct send_buffer* buf = NULL;
if ((buf = (struct send_buffer*)malloc(sizeof(struct send_buffer) + sizeof(char) * data_len)) != NULL)
{
buf->len = data_len;
memcpy(buf->data, "Hello World", data_len);
printf("%d, %s\n", buf->len, buf->data);
}
//使用
...
...
...
//释放
free(buf);
buf = NULL;
1.优点:集合了定长数据包和指针包的优点,数据包的长度可根据需求来进行变化,避免了空间浪费,并且在释放时只需释放数据包的变量即可,避免了多次free操作
可以去试验一下,试验方法如下:
#include <stdio.h>
#include <stdlib.h>
typedef struct send_buf//变长数据包
{
int len;
char data[0];
}send_buf;
typedef struct send_buf_p//指针数据包
{
int len;
char* data;
}send_buf_p;
int main()
{
int data_len = 50;
send_buf *buf = NULL;
char* addr = NULL; //此指针用于保存包的数据部分用于测试两种数据包的区别
if ((buf = (send_buf *)malloc(sizeof(send_buf) + sizeof(char) * data_len)) != NULL)
{
buf->len = data_len;
memcpy(buf->data, "hello world", data_len);
printf("%d %s\n", buf->len, buf->data);
}
addr = buf->data;//在 free 前保存
free(buf);//变长数据包的释放
printf("%d %s\n", buf->len, addr);//测试数据是否还能取得
buf = NULL;
send_buf_p *bufa = NULL;
if ((bufa = (send_buf_p *)malloc(sizeof(send_buf_p))) != NULL)
{
bufa->len = data_len;
if ((bufa->data = (char*)malloc(sizeof(char) * data_len)) != NULL)
{
memcpy(bufa->data, "hello worldaaa", data_len);
printf("%d %s\n", bufa->len, bufa->data);
}
}
addr = bufa->data;//在 free 前保存
free(bufa);//指针数据包的释放,注意此时只释放了这个数据巴并没有释放其里面指向数据的指针
printf("%d %s\n", bufa->len, addr);//测试数据是否还能取得
}
结果如下:
可以看到指针数据包中指向数据那部分的数据如果未释放的话还是能取得的.
2.注意:这是C90标准后的扩展,可能较早的编译器识别不出会报错.
对于 GNU C 增加的扩展, GCC 提供了编译选项来明确的标识出他们:
- -pedantic 选项,那么使用了扩展语法的地方将产生相应的警告信息
- -Wall 使用它能够使GCC产生尽可能多的警告信息
- -Werror, 它要求GCC将所有的警告当成错误进行处理
想更多了解变长数组,可以去:
看看.在此不做太多解释了.
3.case 范围
一般来说 C语言语法是这样的:
switch(常量)
{
case 常量表达式1 :
代码段1;
break;
case 常量表达式2 :
代码段2;
break;
...
...
case 常量表达式n :
代码段n;
break;
default:
代码段n+1;
break;
}
而GNU C语言支持指定一个case区间作为一个标签:
case low ... high:
代码段 a;
break;
//如
case 'A' ... 'Z':
代码段 ;
break;
注意: 在定义这个区间时必须遵循switch语法的规则,即 case 后面的使用规则.只能由:
int型常量、char型常量、enum型常量、sizeof表达式&经过强制类型转换后的浮点型常量
区间之间不能有交集.
注意: const 修饰的不代表常量,只是表示变量是只读而已.
#include <stdio.h>
#define ZERO 0
#define FIVE 5
#define TEN 10
typedef enum number
{
zero = ZERO,
five = FIVE,
ten = TEN
}number;
int main()
{
int i = 1;
switch (i)
{
case zero ... sizeof(int):
printf("范围在 0-4\n");
break;
case five ... (int)10.0:
printf("范围在 5-10\n");
break;
case 'a' ... 'z':
printf("范围在 a-z\n");
}
}
执行结果如下:
4.标号元素
这个特性可能用过,在此可以复习一下:
以往结构体初始化方式:
#include <stdio.h>
typedef struct student
{
int age;
char* name;
}student;
int main()
{
student s1;
s1.name = "bob";
s1.age = 10;
student s2 = {
11,
"anna"
};
}
第一种太繁琐,结构体成员一多就得写许多行.
第二种得按顺序写,开发效率不高.
将两者结合就是第三种:
#include <stdio.h>
typedef struct student
{
int age;
char* name;
}student;
int main()
{
student s3 = {
.name = "jok",
.age = 9
};
}
在Linux内核中的实例:
<drivers/char/mem.c>
static const struct file_operations zero_fops = {
.llseek = zero_lseek,
.read = new_sync_read,
.write = write_zero,
.read_iter = read_iter_zero,
.aio_write = aio_write_zero,
.mmap = mmap_zero,
};
这是一个文件操作函数结构体的初始化,每个成员都初始化为一个函数指针,当file_operations数据结构的定义发生变化时,这种方法仍可以保证已知元素的正确性,对于未初始化的成员的值为0或者为NULL.
5.可变参数宏
先来学习或者温习一下可变参数函数:
可变参数函数在我们刚学C语言时就遇到了,比如说 printf , scanf 函数,这两个函数的参数是不确定的,就是可变参数函数.
注意:任何一个可变参数的函数都可以分为两部分:固定参数和可选参数。至少要有一个固定参数,其声明与普通函数参数声明相同;可选参数由于数目不定(0个或以上),声明时用"…"表示。固定参数和可选参数共同构成可变参数函数的参数列表.
1.可变参数函数使用说明
1.包含相应头文件:
#include <stdarg.h>
2.熟悉相关宏的作用:
va_list
用来保存宏va_start、va_arg和va_end所需信息的一种类型,为了访问变长参数列表中的参数,必须声明va_list类型的一个对象
va_start(v,l)
访问变长参数列表中的参数之前使用的宏,用来初始化用 va_list 声明的对象,始化结果供宏 va_arg 和 va_end 使用
va_arg(v,l)
展开成一个表达式的宏,该表达式具有变长参数列表中下一个参数的值和类型。每次调用va_arg都会修改用va_list声明的对象,从而使该对象指向参数列表中的下一个参数;
注意 : 第二个参数 不支持以下类型:
——char 、signed char 、unsigned char
——short 、unsigned short
——signed short 、short int 、signed short int 、unsigned short int
——float
va_end(v)
该宏使程序能够从变长参数列表用宏 va_start 引用的函数中正常返回
3.举例
#include <stdio.h>
#include <stdarg.h>
int sum(int arg_num, ...)//arg_num 表示参数个数
{
int s = 0;
va_list ap;//定义va_list类型变量
va_start(ap, arg_num);//初始化宏,并获得第二个参数的地址,可以理解为ap现在指向第二个参数的地址
for (int i = 0; i < arg_num; i++)
{
//取得该地址为 "int" 类型的参数值,并使获取下一个参数的地址(ap指向下一个参数)
s += va_arg(ap, int);
}
va_end(ap);//结束轮询并做一些结尾工作
return s;
}
int main()
{
int arr[5] = {1, 2, 3, 4, 5};
int s = sum(5, arr[0], arr[1], arr[2], arr[3], arr[4]);
printf("sum = %d\n", s);
}
注意 : 可变参数函数必须至少有一个参数.
2.可变参数宏
有了上面的可变参数函数和带参宏(宏函数)的已知后,理解这个也不难了.
在GNU C语言中,宏可以接受可变数目的参数, 这主要运用在输出函数里.
<include/linux/printk.h>
#define pr_debug(fmt, ...) \
dynamic_pr_debug(fmt, ##__VA_ARGS__)
"..." 代表一个参数列表, "__VA_ARGS__"是编译器保留字段,预处理时把参数传递给宏.当宏的调用展开时,实际参数就传递给 dynamic_pr_debug 函数中了.
"##"是什么?
## 是一种预处理运算符,用在宏定义中,在宏解析时将实际的参数进行连接。就是将两个标识符粘在一起.
举例:
#include <stdio.h>
#define connect(a, b) a##b
int main()
{
int connect(i, j) = 1;
printf("connect(i, j) = %d\n", ij);
}
结果:
6.属性
GNU C语言允许声明函数属性,变量属性,类型属性,以便于编译器进行特定方面的优化和更仔细的代码检查,特殊属性的语法格式为:
__attribute__ ((attribute-list))
1.函数属性
函数属性有很多,如 noreturn, format 以及 const 等.此外,还可以定义一些和处理器体系结构相关的函数属性,如 ARM 体系结构中可以定义 interrupt(中断) , isr(中断服务程序)等.
这里以noreturn, format 以及 const为例子进行讲解:
1.noreturn
_Noreturn 关键字告诉编译器这个函数不会返回,这让编译器消除了不必要的警告信息,如 die 函数,该函数不会返回.
void __attribute__ ((noreturn)) die(void);
注意: 与 void 相同的是都没有返回值,但 void 会返回到调用处,而 noreturn 不会.
#include <stdio.h>
void func1()
{
}
int __attribute__ ((noreturn)) func2(int i)
{
return ++i;
}
int main()
{
func1();
printf("func1 调用完\n");
int j = func2(1);
printf("func2 调用完 j = %d\n", j);
}
运行结果:
noreturn 使用事项:
1. 主要用于不会返回到调用者函数时:
-
终止程序:例如,exit()函数和abort()函数。这些函数一旦被调用,程序就会立即终止,不会返回到调用者。
-
无限循环:例如,while(1) {}。如果一个函数包含无限循环,那么它也不会返回到调用者。
2.使用限制:
-
只能用于函数声明,不能用于函数定义。
-
必须出现在函数声明的最开始部分,不能出现在其他地方。
-
如果一个函数被声明为_Noreturn,那么它必须确保不会返回到调用者。如果它返回到了调用者,那么结果是未定义的。
2.const
const 会让编译器只调用该函数一次,以后再调用时只需要返回第一次结果即可,从而提高效率.即:让编译器知道这个函数的返回值是一个常量,不会随其他变量的改变而改变.
static inline u32 __attribute_const__ read_cpuid_cachetype(void)
{
return read_cpuid(CTR_EL0);
}
这是一个用于读取CTR_EL0寄存器的值并返回的函数,而 __attribute_const__ 表示这个函数没有副作用,并且不会修改任何全局变量,让编译器可以良好地优化这个函数.
3.format
这个属性会让编译器按照规定的参数表格式规则对该函数参数进行检查,使得在编译器时检测到这些错误,有效提高了程序的可靠性和可读性.如:
<drivers/staging/lustru/include/linux/libcfs>
int libcfs_debug_msg (struct libcfs_debug_msg_data *msgdata,
const char *format1, ...)
__attribute__ ((format (printf, 2, 3)));
这个函数声明了一个format函数属性,它会告诉编译器按照 printf 的参数表的格式规则对该函数参数进行检查. 数字2表示第二个参数为格式化字符串,数字3表示 "..." 里的第一个参数在函数参数总数中排在第几个.
#include <stdio.h>
#include <stdarg.h>
int msg(char* format, int i, ...)
__attribute__ ((format (printf, 1, 3)));
int msg(char* format, int i, ...)
{
va_list ap;
va_start(ap, i);
vprintf(format, ap);
va_end(ap);
}
int main()
{
int i = 1, j = 2;
char k = 'k';
char *a = "hello %d";
//正确调用
msg(a, i, j);
//错误调用
msg(i, a, j);
//错误调用
msg(i, a, k);
}
三个调用里只有第一个能通过,其他的会出现段错误.
2.变量和类型属性
变量属性可以对变量或结构体成员进行属性设置,可以去看看我这篇文章<<变量属性详解>>类型属性常见的属性有 alignment, packed 和 sections 等.
1. alignment
这个属性规定变量或者结构体成员的最小对齐方式,以字节为单位.
struct qib_user_info
{
__u32 spu_userversion;
__u64 spu_base_info;
} __aligned(8) ;
这个结构体让编译器以8字节对齐方式来分配数据结构.
#include <stdio.h>
typedef struct student1
{
int i;
double b;
char a;
}student1;
struct student2
{
int i;
double b;
char a;
} __attribute__((aligned(16)));//对齐方式为16
typedef struct student2 student2;
int main()
{
student1 s1;
student2 s2;
printf("s1地址是%zu\n", &s1);
printf("s2地址是%zu\n", &s2);
printf("s1分配的空间有%zu字节\n", sizeof(student1));
printf("s2分配的空间有%zu字节\n", sizeof(student2));
}
执行结果:
其中 s1 是默认的8字节对齐,而 s2 是16字节对齐,因此他们变量的起始地址地址都是可以整除对齐标准的.参照8字节对齐的s1的空间分配,可以推一下s2为什么是占32个字节.
2.packed
这个属性可以使变量或者结构体成员使用最小的对齐方式,并且不进行任何字节填充,这是与aligned不同的地方,而对变量是以字节对齐,对域是以位对齐.
#include <stdio.h>
typedef struct student1
{
char a;
int num[2] __attribute__ ((packed));
}student1;
struct student2
{
int i;
double b;
char a;
} __attribute__((packed));
typedef struct student2 student2;
typedef struct student3
{
int i;
double b;
char a;
int num[2] __attribute__ ((packed));
}student3;
int main()
{
student1 s1;
student2 s2;
student3 s3;
printf("s1地址是%zu\n", &s1);
printf("s2地址是%zu\n", &s2);
printf("s3地址是%zu\n", &s3);
printf("s1分配的空间有%zu字节\n", sizeof(student1));
printf("s2分配的空间有%zu字节\n", sizeof(student2));
printf("s3分配的空间有%zu字节\n", sizeof(student3));
}
执行结果为:
student1中的 name 成员使用了 packed 属性, 它会存储在变量a的后面,所以一共会占有9字节.
student2使用了 packed 属性, 使其结构体空间分配时不会填充字节,因此占有13个字节.
student3中的 num 成员使用了 packed 属性,此结构体因为只是个别成员被 packed 修饰,其他成员在内存分配时还会遵守默认内存对齐(8字节填充).因此占有32字节.
3.sections
C语言中sections主要用于指定将代码或数据分配到特定的节(section)中,从而可以控制它们在内存中的布局和访问权限.节是指在可执行文件或库中指定代码或数据的虚拟地址范围.
使用attribute的sections可以实现以下几个用途:
1. 优化代码布局:通过将相关的函数或数据放置在相邻的节中,可以提高局部性,从而减少指令缓存(I-cache)的不命中率。这对于提高程序的性能是有益的。
2. 内存映射:通过将代码或数据放置在特定的节中,可以在编译时将它们映射到特定的内存区域。这在嵌入式系统中特别有用,可以实现将代码放置在特定的ROM区域,而数据放置在特定的RAM区域。
3. 分离只读数据和可读写数据:通过将只读数据(如常量字符串)放置在只读节中,可以节省RAM的使用。只读节通常也可以是只读的存储设备上的一部分。
int main()
{
int a = 5, b = 6;
int result;
// 将result变量分配到.data节中
int result __attribute__((section(".data")));
result = a * b;
// 将print_result函数分配到.text节中
void print_result(int result) __attribute__((section(".text")));
print_result(result);
return 0;
}
7.内建函数
内建函数就是编译器内部实现的函数,可直接使用,无需 #include 对应的头文件就能使用,GNU C语言提供了一系列内建函数进行优化,这些内建函数以 "_builtin_"作为函数名前缀.
Linux内核中常见的内建函数:
1. __builtin_constant_p(x)
判断 x 是否在编译时就可以被确定为常量. 是则返回1,否则0;
#define __swab16(x) \
(__builtin_constant_p((__u16)(x)) ? \
__constant_swab16(x) : \
__fswab16(x))
#include <stdio.h>
#define j 0
const int i = 1;
int main()
{
printf("%d\n", __builtin_constant_p(i));
printf("%d\n", __builtin_constant_p(j));
}
结果如下:
2. __builtin_expect(exp, c)
这里的意思是 exp == c 概率很大,用来引导GCC编译器进行条件分支预测.开发人员知道最可能执行哪个分支,并将最有可能执行的分支告诉编译器,让编译器优化指令序列,使指令尽可能地顺序执行,从而提高CPU预取指令的正确率.
#define LIKELY(x) __builtin_expect(!!(x), 1) //x很可能为真
#define UNLIKELY(x) __builtin_expect(!!(x), 0) //x很可能为假
!!(x)表示将 x 转换为对应的逻辑值.
3. __builtin_perfetch(const void *addr, int rw, int locality)
主动进行数据预取,在使用地址 addr 的值之前就把其中值加载带 cache 中,减少读取的延时,从而提高性能.该函数可接受1-3个参数.
第一个参数表示要预取数据的地址.
第二个参数表示读写属性, 1 表示可写, 0 表示只读.
第三个参数表示数据在cache中的时间局部性, 0表示读取完 addr 的之后不用保留在cache中,而 1-3 表示时间局部性逐渐增强.
<include/linux/perfetch.h>
#define prefetch(x) __builtin_prefetch(x)
#define prefetchw(x) __builtin_prefetch(x, 1)
8. asmlinkage
在标准C语言中, 函数的形参在实际传入参数时会涉及参数存放问题.
对于x86结构,函数参数和局部变量被一起分配到函数的局部堆栈里.
<arch/x86/include/asm/linkage.h>
#define asmlinkage CPP_ASMLINKAGE __attribute__((regparm(0)))
__attribute__((regparm(0))): 告诉编译器该函数不需要通过任何寄存器来传递参数,只通过堆栈来传递.
而对于ARM来说,函数参数的传递有一套ATPCS标准,即通过寄存器来传递.ARM中的R1 - R4 寄存器存放传入参数,当参数超过5个时,多余的参数被存放在局部堆栈中.所以,ARM平台没有定义asmlinkage.
9. UL
在Linux内核代码中,经常会看到一些数字的定义使用了 UL 后缀修饰.数字常量会被隐形定义为int 类型,两个 int 类型相加的结果可能会发生溢出, 因此使用UL强制把 int 类型数据转换为 unsigned long 类型, 这是因为为了保证运算过程不会因为 int 的位数不同而导致溢出.
1 //表示有符号整型数字1
1UL //表示无符号长整型数字1
10. 结语
在Linux内核代码中使用的 C语言技巧与拓展用法远不止这一点,而想要真正去了解学习这些用法,就必须得通过大量地代码阅读与自我试验,而这只是学习底层内核代码的门槛而已,加油!
如有错误,欢迎指正!