tee系统调用用法

tee() 是 Linux 特有的系统调用,用于在两个管道之间高效复制数据(零拷贝),不消耗原始数据。以下是详细用法:

函数原型

1
2
#include <fcntl.h>
ssize_t tee(int fd_in, int fd_out, size_t len, unsigned int flags);

参数说明

参数 说明
fd_in 源管道的读取端文件描述符(必须是管道)
fd_out 目标管道的写入端文件描述符(必须是管道)
len 要复制的最大字节数
flags 控制行为的标志位(常用 SPLICE_F_MOVESPLICE_F_NONBLOCK

关键特性

  1. 零拷贝:数据在内核管道缓冲区移动,避免用户空间复制。
  2. 非消耗性:源管道 fd_in 的数据在复制后仍可被读取。
  3. 仅限管道fd_infd_out 必须是管道(通过 pipe() 创建)。

常用标志位

标志 说明
SPLICE_F_MOVE 尝试移动内存页(实际效果有限)
SPLICE_F_NONBLOCK 非阻塞操作(立即返回,不等待数据)
SPLICE_F_MORE 提示后续有更多数据(用于网络优化)
SPLICE_F_GIFT 未使用(保留)

返回值

  • 成功:复制的字节数(可能小于 len)。
  • 失败:返回 -1,并设置 errno(常见错误见下表)。

常见错误码

错误码 说明
EAGAIN 非阻塞模式下无数据可用
EINVAL 文件描述符不是管道/无效标志
ENOMEM 内存不足
ESPIPE 文件描述符不支持 splice 操作

使用示例

以下代码演示如何将标准输入复制到两个管道(类似 tee 命令):

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
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>

int main() {
int pipe1[2], pipe2[2];
pipe(pipe1); // 创建管道1: [0]=读端, [1]=写端
pipe(pipe2); // 创建管道2

while (1) {
// 从 stdin (0) 复制数据到 pipe1 的写端
ssize_t bytes = splice(STDIN_FILENO, NULL, pipe1[1], NULL, 4096, SPLICE_F_MOVE);
if (bytes <= 0) break;

// 将 pipe1 的数据复制到 pipe2(不消耗原始数据)
ssize_t tee_bytes = tee(pipe1[0], pipe2[1], bytes, SPLICE_F_NONBLOCK);
if (tee_bytes < 0) {
if (errno == EAGAIN) continue; // 非阻塞重试
perror("tee failed");
break;
}

// 从 pipe1 读取原始数据(可选)
char buf[4096];
read(pipe1[0], buf, bytes); // 实际使用应检查返回值

// 从 pipe2 读取复制数据(示例)
read(pipe2[0], buf, tee_bytes);
}

close(pipe1[0]); close(pipe1[1]);
close(pipe2[0]); close(pipe2[1]);
return 0;
}

典型应用场景

  1. 数据分路:将输入同时转发到多个处理流程。
    1
    2
    3
    4
    5
    graph LR
    A[输入源] -->|splice| B[管道1]
    B -->|tee| C[管道2]
    B --> 处理流程1
    C --> 处理流程2
  2. 零拷贝代理:高效转发网络数据(需结合 splice)。
  3. 日志记录:在数据处理过程中同步复制日志流。

注意事项

  1. 必须使用管道:非管道文件描述符会返回 EINVAL
  2. 数据未真正复制:内核仅传递缓冲区引用,需确保后续读取。
  3. 结合 splice() 使用:完整零拷贝链通常需组合 splicetee
  4. 阻塞行为:默认阻塞直到有数据,使用 SPLICE_F_NONBLOCK 可非阻塞。

💡 提示:实际开发中应检查所有系统调用的返回值,并处理 EINTR(信号中断)。

personality系统调用

personality() 是 Linux 特有的系统调用,用于设置或获取进程的执行域(execution domain),控制进程的二进制接口(ABI)行为、地址空间布局等特性。以下是详细说明:


函数原型

1
2
#include <sys/personality.h>
int personality(unsigned long persona);

参数说明

参数 说明
persona 要设置的个性标志(或 0xffffffff 表示获取当前值)

返回值

  • 成功:返回之前设置的个性值
  • 失败:返回 -1 并设置 errno

常见个性标志(Personality Flags)

1. 执行域(ABI 控制)

标志 说明
PER_LINUX 标准 Linux ABI(默认)
PER_LINUX32 32 位 Linux ABI
PER_SVR4 System V Release 4 兼容模式
PER_BSD BSD 兼容模式
PER_MASK 掩码(0x0fffffff)用于提取基础域

2. 行为控制标志

标志 说明
ADDR_NO_RANDOMIZE 禁用 ASLR(地址空间随机化)
MMAP_PAGE_ZERO 映射第 0 页(传统行为)
ADDR_COMPAT_LAYOUT 使用兼容的虚拟地址空间布局
READ_IMPLIES_EXEC 读权限隐含执行权限(传统行为)
STICKY_TIMEOUTS select() 调用不修改超时参数

核心功能

  1. 切换 ABI 环境
    允许在 64 位系统中运行 32 位程序(通过 PER_LINUX32)。

  2. 禁用 ASLR
    通过 ADDR_NO_RANDOMIZE 固定内存地址,便于调试:

    1
    personality(personality(0xffffffff) | ADDR_NO_RANDOMIZE);
  3. 修改内存布局
    控制 mmap() 等系统调用的行为(如 MMAP_PAGE_ZERO)。

  4. 模拟其他操作系统
    支持 BSD、SVR4 等兼容模式。


使用示例

示例 1:获取当前个性值

1
2
unsigned long current_persona = personality(0xffffffff);
printf("Current persona: 0x%lx\n", current_persona);

示例 2:临时禁用 ASLR

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <sys/personality.h>
#include <stdio.h>

int main() {
// 保存原个性设置
unsigned long orig_persona = personality(0xffffffff);

// 禁用 ASLR
if (personality(orig_persona | ADDR_NO_RANDOMIZE) < 0) {
perror("personality");
return 1;
}

printf("ASLR disabled\n");
// 此处运行需要固定地址的代码

// 恢复原设置
personality(orig_persona);
return 0;
}

示例 3:切换到 32 位 ABI 环境

1
2
3
4
5
6
// 在 64 位系统运行 32 位程序前设置
if (personality(PER_LINUX32) < 0) {
perror("personality");
exit(1);
}
execl("./32bit_program", "32bit_program", NULL);

错误处理

错误码 说明
EPERM 无权限修改个性(非 root)
EINVAL 无效的 persona

典型应用场景

  1. 调试器
    禁用 ASLR 以稳定复现内存地址问题。

  2. 兼容层
    实现 Linux 运行其他系统二进制文件(如 Wine、BSD 兼容层)。

  3. 安全加固
    强制启用 ASLR(清除 ADDR_NO_RANDOMIZE)。

  4. 嵌入式系统
    定制内存布局以适应资源限制。


注意事项

  1. 权限要求
    修改个性通常需要 CAP_SYS_ADMIN 能力(即 root 权限)。

  2. 继承性
    个性设置通过 fork() 继承,但 execve()重置为默认值(除非使用 AT_FLAGS 特殊标记)。

  3. 架构依赖
    某些标志(如 PER_LINUX32)仅在 x86、ARM 等特定架构有效。

  4. 内核版本
    部分标志(如 ADDR_COMPAT_LAYOUT)需内核 ≥ 2.6.12。

⚠️ 警告:随意修改个性可能破坏程序稳定性!仅用于特定兼容/调试场景。

Linux根据dentry找到文件名称

在 Linux 内核中,通过 dentry(目录项)结构获取文件名是一个常见的操作。以下是详细说明和实现方法:


核心数据结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct dentry {
// ...
struct qstr d_name; // 包含文件名的关键字段
// ...
};

struct qstr {
union {
struct {
HASH_LEN_DECLARE;
};
u64 hash_len;
};
const unsigned char *name; // 文件名字符串的指针
};

获取文件名的三种方法

方法 1:直接访问 d_name 字段(最常用)

1
2
3
4
5
6
#include <linux/dcache.h>

const char *get_name_from_dentry(struct dentry *dentry) {
// 直接返回文件名指针
return dentry->d_name.name;
}

特点

  • 直接访问内核数据结构
  • 不处理路径,只返回当前目录项的文件名
  • 文件名不以空字符结尾,需结合长度使用

方法 2:使用 dentry_path_raw()(获取完整路径)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <linux/dcache.h>
#include <linux/slab.h> // kmalloc/kfree

char *get_full_path(struct dentry *dentry) {
char *buffer = kmalloc(PATH_MAX, GFP_KERNEL);
if (!buffer) return NULL;

char *path = dentry_path_raw(dentry, buffer, PATH_MAX);
if (IS_ERR(path)) {
kfree(buffer);
return NULL;
}
return path; // 返回完整路径(如 "/home/user/file.txt")
}

特点

  • 返回从根目录开始的完整路径
  • 需要手动分配/释放缓冲区
  • 路径以空字符结尾

方法 3:递归构建路径(内核链表操作)

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
static void reverse_path(struct dentry *dentry, struct list_head *list) {
while (dentry && !IS_ROOT(dentry)) {
list_add(&dentry->d_name.list, list); // 添加到链表
dentry = dentry->d_parent; // 向父目录递归
}
}

char *build_path_from_dentry(struct dentry *dentry) {
LIST_HEAD(name_list);
char *path = kmalloc(PATH_MAX, GFP_KERNEL);
char *p = path;

reverse_path(dentry, &name_list);

// 从链表构建路径
struct list_head *pos;
list_for_each(pos, &name_list) {
struct dentry *d = list_entry(pos, struct dentry, d_name.list);
int len = d->d_name.len;
memcpy(p, d->d_name.name, len);
p += len;
*p++ = '/';
}
*p = '\0';
return path;
}

关键注意事项

  1. 非空终止字符串
    dentry->d_name.name 不是以 \0 结尾的 C 字符串,必须结合长度使用:

    1
    2
    3
    printk(KERN_INFO "Filename: %.*s\n", 
    dentry->d_name.len,
    dentry->d_name.name);
  2. 锁机制
    在访问 dentry 时通常需要持有 d_lock 自旋锁:

    1
    2
    3
    spin_lock(&dentry->d_lock);
    // 安全访问 dentry 字段
    spin_unlock(&dentry->d_lock);
  3. 引用计数
    确保 dentry 在使用期间不会被释放:

    1
    2
    3
    dget(dentry);    // 增加引用计数
    // 操作 dentry...
    dput(dentry); // 减少引用计数
  4. 负目录项(Negative Dentries)
    即使文件不存在(负 dentry),d_name 仍然有效:

    1
    2
    3
    4
    5
    if (dentry->d_inode) {
    // 文件存在
    } else {
    // 负 dentry(文件不存在)
    }

实际应用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <linux/dcache.h>
#include <linux/fs.h>

void debug_dentry_info(struct dentry *dentry) {
spin_lock(&dentry->d_lock);

printk(KERN_DEBUG "Dentry info:\n");
printk(KERN_DEBUG " Name: %.*s\n",
dentry->d_name.len, dentry->d_name.name);
printk(KERN_DEBUG " Parent: %pd\n", dentry->d_parent);
printk(KERN_DEBUG " Inode: %lu\n",
dentry->d_inode ? dentry->d_inode->i_ino : 0);

spin_unlock(&dentry->d_lock);
}

性能优化技巧

  1. 避免完整路径构建
    除非必要,否则优先使用 dentry->d_name 而非完整路径

  2. 使用 d_path() 替代 dentry_path_raw()
    更安全的路径构建函数:

    1
    char *path = d_path(&dentry->d_path, buffer, PATH_MAX);
  3. RCU 安全访问
    在 RCU 临界区中访问:

    1
    2
    3
    4
    5
    6
    rcu_read_lock();
    struct dentry *d = __d_lookup_rcu(parent, &name);
    if (d) {
    // 安全访问 d->d_name
    }
    rcu_read_unlock();

典型应用场景

  1. 文件系统监控
    在 inotify/fanotify 中记录访问路径

  2. 调试工具
    在内核模块中打印文件访问信息

  3. 安全模块
    在 LSM(Linux Security Module)中检查文件路径

  4. 文件系统实现
    在自定义文件系统中处理路径逻辑

⚠️ 警告:直接操作 dentry 是内核级开发,不当使用可能导致系统崩溃。建议在开发环境中充分测试。