preadv2系统调用及示例

好的,我们继续按照您的要求学习 Linux 系统编程中的重要函数。这次我们介绍 preadv2、pwritev2 和 pkey_mprotect。

函数 1: preadv2

1. 函数介绍

preadv2 (pread vector 2) 是 preadv 系统调用的扩展版本。它结合了 pread(带偏移量读取)和 readv(分散读取)的优点,并引入了一个新的 flags 参数,提供了更灵活的 I/O 控制选项。

data-ad-format="fluid" data-ad-layout-key="-7k+ex-4a-9w+4a">

简单来说,preadv2 允许你从文件的指定偏移量开始,将数据分散读入到多个不连续的缓冲区中,同时还能指定一些高级 I/O 行为(通过 flags)。

2. 函数原型

1
2
3
4
5
6
#define _GNU_SOURCE // 必须定义以使用 preadv2
#include <sys/uio.h> // struct iovec
#include <unistd.h> // ssize_t

ssize_t preadv2(int fd, const struct iovec *iov, int iovcnt, off_t offset, int flags);

3. 功能

  • 从文件描述符 fd 指定的文件中,从绝对偏移量 offset 开始读取数据。

  • 将读取的数据分散存储到由 iov 和 iovcnt 指定的多个缓冲区中。

  • 不修改文件的当前读写位置指针(lseek 位置)。

  • 根据 flags 参数执行特定的 I/O 操作。

4. 参数

  • int fd: 有效的文件描述符。

  • const struct iovec *iov: 指向 struct iovec 数组的指针,描述了多个分散的缓冲区。

  • int iovcnt: iov 数组中元素的个数。

  • off_t offset: 在文件中开始读取的绝对偏移量(以字节为单位)。必须是非负数。

int flags: 控制 I/O 行为的标志。可以是以下值的按位或组合:

  • 0: 默认行为,等同于 preadv。

  • RWF_HIPRI: 尝试使用高优先级/实时 I/O(如果内核和设备支持)。

  • RWF_DSYNC: 要求 I/O 操作具有数据同步持久性(类似于 O_DSYNC)。

  • RWF_SYNC: 要求 I/O 操作具有文件同步持久性(类似于 O_SYNC)。

  • RWF_NOWAIT: 非阻塞。如果 I/O 无法立即完成(例如,需要从磁盘读取而数据不在页缓存中),则不等待,立即返回错误 EAGAIN。这需要内核和文件系统支持。

  • RWF_APPEND: 强制将写入追加到文件末尾(仅对 pwritev2 有效)。

5. 返回值

  • 成功时: 返回实际读取的总字节数(0 表示 EOF)。

  • 失败时: 返回 -1,并设置 errno。

函数 2: pwritev2

1. 函数介绍

pwritev2 (pwrite vector 2) 是 pwritev 系统调用的扩展版本。它结合了 pwrite(带偏移量写入)和 writev(集中写入)的优点,并同样引入了 flags 参数。

简单来说,pwritev2 允许你从多个不连续的缓冲区中收集数据,并将其写入到文件的指定偏移量处,同时还能指定一些高级 I/O 行为(通过 flags)。

2. 函数原型

1
2
3
4
5
6
#define _GNU_SOURCE
#include <sys/uio.h>
#include <unistd.h>

ssize_t pwritev2(int fd, const struct iovec *iov, int iovcnt, off_t offset, int flags);

3. 功能

  • 从由 iov 和 iovcnt 指定的多个缓冲区中收集数据。

  • 将收集到的数据写入到文件描述符 fd 指定的文件中,从绝对偏移量 offset 开始写入。

  • 不修改文件的当前读写位置指针(lseek 位置)。

  • 根据 flags 参数执行特定的 I/O 操作。

4. 参数

  • int fd: 有效的文件描述符。

  • const struct iovec *iov: 指向 struct iovec 数组的指针,描述了多个包含数据的缓冲区。

  • int iovcnt: iov 数组中元素的个数。

off_t offset: 在文件中开始写入的绝对偏移量(以字节为单位)。必须是非负数。

  • 如果文件以 O_APPEND 模式打开,或者 flags 中设置了 RWF_APPEND,则 offset 参数会被忽略,数据总是被写入到文件末尾。

int flags: 控制 I/O 行为的标志。可以是以下值的按位或组合:

  • 0: 默认行为,等同于 pwritev。

  • RWF_HIPRI: 尝试使用高优先级/实时 I/O。

  • RWF_DSYNC: 要求数据同步持久性。

  • RWF_SYNC: 要求文件同步持久性。

  • RWF_NOWAIT: 非阻塞。如果 I/O 无法立即完成,立即返回错误 EAGAIN。

  • RWF_APPEND: 强制将写入追加到文件末尾,即使文件没有以 O_APPEND 打开。

5. 返回值

  • 成功时: 返回实际写入的总字节数。

  • 失败时: 返回 -1,并设置 errno。

函数 3: pkey_mprotect

1. 函数介绍

pkey_mprotect 是 mprotect 系统调用的扩展,用于将一个内存区域与一个特定的内存保护键(Protection Key, pkey)相关联。

回忆一下 pkey_alloc/free:它们用于获取和释放 pkey 编号。pkey_mprotect 则是将这个编号应用到具体的内存区域上。

一旦内存区域通过 pkey_mprotect 与一个 pkey 关联,对该区域的访问权限就不仅受传统的 PROT_READ/PROT_WRITE/PROT_EXEC 控制,还受该 pkey 在 CPU 的 PKRU(Protection Key Rights User)寄存器中设置的权限控制。加粗样式

2. 函数原型

1
2
3
4
5
#define _GNU_SOURCE
#include <sys/mman.h> // 包含 MPK 相关常量

int pkey_mprotect(void *addr, size_t len, int prot, int pkey);

3. 功能

  • 修改从地址 addr 开始、长度为 len 字节的内存区域的访问权限。

  • 将该内存区域与保护键 pkey(由 pkey_alloc 获得)进行关联。

  • 设置该区域的基本权限为 prot(PROT_READ, PROT_WRITE, PROT_EXEC 的组合)。

4. 参数

  • void *addr: 要修改的内存区域的起始地址。必须是页对齐的。

  • size_t len: 内存区域的长度(以字节为单位)。会向上舍入到最近的页边界。

  • int prot: 新的内存保护标志。可以是 PROT_NONE, PROT_READ, PROT_WRITE, PROT_EXEC 及其按位或组合。

  • int pkey: 通过 pkey_alloc 获得的保护键编号(0-15)。

5. 返回值

  • 成功时: 返回 0。

  • 失败时: 返回 -1,并设置 errno。

示例代码

示例 1:preadv2 和 pwritev2 的基本使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
// preadv2_pwritev2_example.c
#define _GNU_SOURCE
#include <sys/uio.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>

#define FILENAME "test_piov2.txt"

int main() {
int fd;
char buf1&#91;20], buf2&#91;30], buf3&#91;50];
struct iovec iov_w&#91;2], iov_r&#91;3];
ssize_t bytes_written, bytes_read;

// 1. 创建并写入测试文件 (使用传统 write)
fd = open(FILENAME, O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd == -1) {
perror("open for write");
exit(EXIT_FAILURE);
}

const char *data1 = "Part One: Hello, ";
const char *data2 = "preadv2 and pwritev2 World!\n";
iov_w&#91;0].iov_base = (void*)data1;
iov_w&#91;0].iov_len = strlen(data1);
iov_w&#91;1].iov_base = (void*)data2;
iov_w&#91;1].iov_len = strlen(data2);

bytes_written = writev(fd, iov_w, 2);
if (bytes_written == -1) {
perror("writev");
close(fd);
exit(EXIT_FAILURE);
}
printf("Written %zd bytes using writev.\n", bytes_written);
close(fd);

// 2. 使用 preadv2 读取
fd = open(FILENAME, O_RDONLY);
if (fd == -1) {
perror("open for read");
exit(EXIT_FAILURE);
}

// 初始化读取缓冲区
memset(buf1, '.', sizeof(buf1) - 1); buf1&#91;sizeof(buf1)-1] = '\0';
memset(buf2, '.', sizeof(buf2) - 1); buf2&#91;sizeof(buf2)-1] = '\0';
memset(buf3, '.', sizeof(buf3) - 1); buf3&#91;sizeof(buf3)-1] = '\0';

iov_r&#91;0].iov_base = buf1;
iov_r&#91;0].iov_len = sizeof(buf1) - 1;
iov_r&#91;1].iov_base = buf2;
iov_r&#91;1].iov_len = sizeof(buf2) - 1;
iov_r&#91;2].iov_base = buf3;
iov_r&#91;2].iov_len = sizeof(buf3) - 1;

// 从偏移量 0 开始读取,使用默认标志
bytes_read = preadv2(fd, iov_r, 3, 0, 0);
if (bytes_read == -1) {
perror("preadv2");
close(fd);
exit(EXIT_FAILURE);
}
printf("\nRead %zd bytes using preadv2 from offset 0:\n", bytes_read);
printf("Buffer 1: '%s'\n", buf1);
printf("Buffer 2: '%s'\n", buf2);
printf("Buffer 3: '%s'\n", buf3);

close(fd);

// 3. 使用 pwritev2 追加写入
fd = open(FILENAME, O_WRONLY); // 不用 O_APPEND
if (fd == -1) {
perror("open for write (again)");
exit(EXIT_FAILURE);
}

const char *append1 = "Appended via ";
const char *append2 = "pwritev2 with RWF_APPEND flag.\n";
struct iovec iov_a&#91;2];
iov_a&#91;0].iov_base = (void*)append1;
iov_a&#91;0].iov_len = strlen(append1);
iov_a&#91;1].iov_base = (void*)append2;
iov_a&#91;1].iov_len = strlen(append2);

// 使用 RWF_APPEND 标志强制追加,忽略 offset
bytes_written = pwritev2(fd, iov_a, 2, 0, RWF_APPEND);
if (bytes_written == -1) {
perror("pwritev2 with RWF_APPEND");
close(fd);
exit(EXIT_FAILURE);
}
printf("\nAppended %zd bytes using pwritev2 with RWF_APPEND.\n", bytes_written);

close(fd);

// 4. 验证文件内容
printf("\n--- Final file content ---\n");
fd = open(FILENAME, O_RDONLY);
if (fd != -1) {
char final_buf&#91;200];
ssize_t n = read(fd, final_buf, sizeof(final_buf) - 1);
if (n > 0) {
final_buf&#91;n] = '\0';
printf("%s", final_buf);
}
close(fd);
}

// unlink(FILENAME); // 可选:清理文件
return 0;
}

代码解释:

创建一个测试文件,并使用 writev 写入一些初始内容。

重新打开文件进行读取。

使用 preadv2(fd, iov_r, 3, 0, 0) 从文件偏移量 0 开始,将数据分散读入三个缓冲区。flags 为 0,表示默认行为。

打开文件进行写入(非 O_APPEND 模式)。

使用 pwritev2(fd, iov_a, 2, 0, RWF_APPEND) 将数据写入文件。尽管 offset 是 0,但由于使用了 RWF_APPEND 标志,数据被追加到了文件末尾。

重新读取并打印文件内容以验证操作结果。

示例 2:pkey_mprotect 结合 pkey_alloc/free 使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
// pkey_mprotect_example.c
#define _GNU_SOURCE
#include <sys/mman.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <signal.h>
#include <setjmp.h>

static jmp_buf jmp_env;
static volatile sig_atomic_t sigsegv_caught = 0;

void sigsegv_handler(int sig) {
sigsegv_caught = 1;
longjmp(jmp_env, 1);
}

// Conceptual PKRU manipulation (requires inline assembly in real code)
// For demonstration, we'll just print what would happen.
void set_pkey_access(int pkey, int disable_access) {
printf(" &#91;Concept] Modifying PKRU for pkey %d: %s\n",
pkey, disable_access ? "DISABLE access" : "ENABLE access");
// Real code would involve inline assembly to write to PKRU register
}

int main() {
// Check for MPK support conceptually
if (sysconf(_SC_MPKEY) <= 0) {
fprintf(stderr, "MPK not supported by sysconf.\n");
exit(EXIT_FAILURE);
}

struct sigaction sa;
sa.sa_handler = sigsegv_handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_RESTART;
if (sigaction(SIGSEGV, &sa, NULL) == -1) {
perror("sigaction");
exit(EXIT_FAILURE);
}

size_t page_size = getpagesize();
size_t len = page_size;
void *addr;

// 1. Allocate memory
addr = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (addr == MAP_FAILED) {
perror("mmap");
exit(EXIT_FAILURE);
}
printf("Allocated %zu bytes at %p\n", len, addr);

// 2. Write some data
strcpy((char*)addr, "This memory is protected by a pkey.");
printf("Written data: %s\n", (char*)addr);

// 3. Allocate a protection key
int pkey = pkey_alloc(0, 0);
if (pkey == -1) {
if (errno == EOPNOTSUPP) {
printf("MPK not supported on this hardware/kernel.\n");
munmap(addr, len);
exit(EXIT_FAILURE);
} else {
perror("pkey_alloc");
munmap(addr, len);
exit(EXIT_FAILURE);
}
}
printf("Allocated pkey: %d\n", pkey);

// 4. Associate memory with the pkey using pkey_mprotect
printf("\n--- Associating memory with pkey %d ---\n", pkey);
if (pkey_mprotect(addr, len, PROT_READ | PROT_WRITE, pkey) == -1) {
perror("pkey_mprotect");
pkey_free(pkey);
munmap(addr, len);
exit(EXIT_FAILURE);
}
printf("Memory successfully associated with pkey %d.\n", pkey);

// 5. Disable access via PKRU (conceptual)
printf("\n--- Disabling access to pkey %d via PKRU ---\n", pkey);
set_pkey_access(pkey, 1); // Conceptual call

// 6. Try to access protected memory (should trigger SIGSEGV)
printf("\n--- Attempting to READ from protected memory ---\n");
sigsegv_caught = 0;

if (setjmp(jmp_env) == 0) {
printf(" Trying to read from %p...\n", addr);
volatile char first_char = *((char*)addr);
printf(" ERROR: Read succeeded (first char: %c). This should not happen!\n", first_char);
} else {
if (sigsegv_caught) {
printf(" SUCCESS: SIGSEGV caught. Access correctly denied by pkey.\n");
} else {
printf(" Unexpected longjmp.\n");
}
}

// 7. Re-enable access
printf("\n--- Re-enabling access to pkey %d via PKRU ---\n", pkey);
set_pkey_access(pkey, 0); // Conceptual call

// 8. Try to access memory again (should succeed)
printf("\n--- Attempting to access memory again (should succeed now) ---\n");
printf(" Reading from %p: %.50s\n", addr, (char*)addr);

// 9. Cleanup
if (pkey_free(pkey) == -1) {
perror("pkey_free");
}
if (munmap(addr, len) == -1) {
perror("munmap");
}

printf("\nPkey_mprotect example finished.\n");
return 0;
}

**代码解释 **(概念性):

设置信号处理和 setjmp/longjmp 用于捕获 SIGSEGV。

使用 mmap 分配一页内存。

写入一些测试数据。

调用 pkey_alloc(0, 0) 获取一个 pkey。

关键步骤: 调用 pkey_mprotect(addr, len, PROT_READ | PROT_WRITE, pkey) 将分配的内存区域与获取的 pkey 关联起来。

概念性操作: 模拟通过修改 PKRU 寄存器来禁用对这个 pkey 的访问。

尝试读取受保护的内存,预期会触发 SIGSEGV。

概念性操作: 模拟重新启用对这个 pkey 的访问。

再次尝试读取,这次应该成功。

清理资源(释放 pkey 和内存)。

重要提示与注意事项:

内核版本:

  • preadv2/pwritev2: Linux 内核 4.6+。

  • pkey_mprotect/pkey_alloc/pkey_free: Linux 内核 4.9+ (MPK)。

glibc 版本: 需要 glibc 2.27+ 才能直接使用这些函数。

硬件支持: pkey_* 函数需要 CPU 支持(如 Intel x86_64 Skylake 及更新架构)。

_GNU_SOURCE: 必须定义此宏才能使用这些扩展函数。

flags 参数: preadv2/pwritev2 的 flags 提供了强大的 I/O 控制能力,特别是 RWF_NOWAIT(非阻塞)和 RWF_APPEND。

pkey_mprotect 是核心: 它是将 pkey 机制应用到实际内存区域的关键步骤。仅仅 pkey_alloc 是不够的。

PKRU 操作: 真正控制 pkey 权限需要直接操作 CPU 的 PKRU 寄存器,这通常需要内联汇编,比较复杂。

错误处理: 始终检查返回值,特别是 pkey_* 函数可能返回 EOPNOTSUPP。

总结:

preadv2 和 pwritev2 是对现有 I/O 系统调用的有力增强,通过引入 flags 参数,提供了更细粒度的控制,如非阻塞 I/O 和强制追加写入。

pkey_mprotect 是内存保护键(MPK)技术的核心 API 之一,它允许将特定的内存区域与一个 pkey 绑定,从而实现比传统 mprotect 更快速、更灵活的内存访问控制。结合 pkey_alloc/free 和对 PKRU 寄存器的操作,可以构建出高性能的内存安全机制。

这三个函数都代表了 Linux 系统编程向更高性能、更细粒度控制发展的趋势。

preadv2 函数

1. 函数介绍

preadv2 是 preadv 的增强版本,支持额外的标志参数,提供更多的控制选项。它是Linux 4.6引入的新特性。

2. 函数原型

1
2
3
4
#define _GNU_SOURCE
#include <sys/uio.h>
ssize_t preadv2(int fd, const struct iovec *iov, int iovcnt, off_t offset, int flags);

3. 功能

与 preadv 类似,但从指定位置读取数据到多个缓冲区,并支持额外的控制标志。

4. 参数

  • int fd: 文件描述符

  • *const struct iovec iov: iovec结构体数组

  • int iovcnt: iov数组元素个数

  • off_t offset: 文件偏移量

  • int flags: 控制标志(如RWF_HIPRI, RWF_DSYNC等)

5. 返回值

  • 成功: 返回实际读取的总字节数

  • 失败: 返回-1,并设置errno

6. 相似函数,或关联函数

  • preadv: 基本版本

  • pwritev2: 对应的写入函数

  • read: 基本读取函数

7. 示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
#define _GNU_SOURCE
#include <sys/uio.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>

/**
* 演示preadv2的基本使用
* 注意:需要Linux 4.6+内核支持
*/
int demo_preadv2_basic() {
int fd;
struct iovec iov&#91;2];
char buf1&#91;30], buf2&#91;20];
ssize_t bytes_read;

printf("=== preadv2 基本使用示例 ===\n");

// 创建测试文件
fd = open("test_preadv2.txt", O_CREAT | O_WRONLY | O_TRUNC, 0644);
if (fd == -1) {
perror("创建测试文件失败");
return -1;
}

const char *test_data = "This is test data for preadv2 function demonstration.";
write(fd, test_data, strlen(test_data));
close(fd);

// 打开文件进行读取
fd = open("test_preadv2.txt", O_RDONLY);
if (fd == -1) {
perror("打开文件失败");
return -1;
}

// 设置iovec数组
iov&#91;0].iov_base = buf1;
iov&#91;0].iov_len = sizeof(buf1) - 1;
iov&#91;1].iov_base = buf2;
iov&#91;1].iov_len = sizeof(buf2) - 1;

// 使用preadv2读取数据(flags设为0表示默认行为)
bytes_read = preadv2(fd, iov, 2, 0, 0);
if (bytes_read == -1) {
if (errno == ENOSYS) {
printf("系统不支持 preadv2 函数\n");
close(fd);
unlink("test_preadv2.txt");
return 0;
}
perror("preadv2 失败");
close(fd);
unlink("test_preadv2.txt");
return -1;
}

printf("preadv2 成功读取 %zd 字节\n", bytes_read);

// 添加字符串结束符并显示结果
buf1&#91;iov&#91;0].iov_len] = '\0';
buf2&#91;iov&#91;1].iov_len] = '\0';

printf("缓冲区1: %s\n", buf1);
printf("缓冲区2: %s\n", buf2);

close(fd);
unlink("test_preadv2.txt");
return 0;
}

/**
* 演示preadv2的高级特性(如果系统支持)
*/
int demo_preadv2_advanced() {
int fd;
struct iovec iov&#91;1];
char buffer&#91;100];
ssize_t bytes_read;

printf("\n=== preadv2 高级特性示例 ===\n");
printf("preadv2 支持的标志包括:\n");
printf(" RWF_HIPRI: 高优先级I/O\n");
printf(" RWF_DSYNC: 数据同步写入\n");
printf(" RWF_SYNC: 同步写入\n");
printf(" RWF_NOWAIT: 非阻塞操作\n");
printf(" RWF_APPEND: 追加模式写入\n");

// 创建测试文件
fd = open("advanced_test.txt", O_CREAT | O_WRONLY | O_TRUNC, 0644);
if (fd == -1) {
perror("创建测试文件失败");
return -1;
}

const char *test_data = "Advanced preadv2 test data for feature demonstration.";
write(fd, test_data, strlen(test_data));
close(fd);

fd = open("advanced_test.txt", O_RDONLY);
if (fd == -1) {
perror("打开文件失败");
return -1;
}

// 设置iovec
iov&#91;0].iov_base = buffer;
iov&#91;0].iov_len = sizeof(buffer) - 1;

// 尝试使用RWF_NOWAIT标志(非阻塞读取)
bytes_read = preadv2(fd, iov, 1, 0, RWF_NOWAIT);
if (bytes_read == -1) {
if (errno == EAGAIN || errno == EWOULDBLOCK) {
printf("非阻塞操作:数据暂时不可用\n");
} else if (errno == ENOSYS) {
printf("系统不支持 RWF_NOWAIT 标志\n");
} else {
printf("preadv2 with RWF_NOWAIT 失败: %s\n", strerror(errno));
}
} else {
buffer&#91;bytes_read] = '\0';
printf("非阻塞读取成功: %s\n", buffer);
}

close(fd);
unlink("advanced_test.txt");
return 0;
}

int main() {
printf("preadv2 需要 Linux 4.6+ 内核支持\n");

if (demo_preadv2_basic() == 0) {
demo_preadv2_advanced();
printf("\n=== preadv2 使用总结 ===\n");
printf("优点:支持额外控制标志,更灵活的I/O控制\n");
printf("注意:需要较新内核版本支持\n");
}
return 0;
}
data-ad-format="auto" data-full-width-responsive="true">