mknod函数 Link to heading
好的,我们继续学习 Linux 系统编程中的重要函数。这次我们介绍 mknod
函数,它用于创建特殊文件,如设备文件(字符设备和块设备)和 FIFO(命名管道)。
1. 函数介绍 Link to heading
mknod
是一个 Linux 系统调用,用于创建特殊文件节点。这些特殊文件包括:
- 字符设备文件 (Character Device Files): 提供对字符流设备的访问,数据按顺序读写,不经过缓冲区。例如,终端 (
/dev/tty
), 串口 (/dev/ttyS0
)。 - 块设备文件 (Block Device Files): 提供对块设备的访问,数据以固定大小的块进行读写,通常有缓冲。例如,硬盘 (
/dev/sda
), CD-ROM (/dev/cdrom
)。 - FIFO (Named Pipes): 用于进程间通信的命名管道,允许无亲缘关系的进程进行单向数据传输。
- Unix 域套接字 (在某些系统上): 用于同一主机上进程间通信的套接字文件。
mknod
是系统管理员和设备驱动程序开发者的重要工具。它允许创建与硬件设备或内核功能相对应的文件系统入口点。普通用户通常不需要直接使用 mknod
,因为设备文件通常在系统安装或设备插拔时由系统自动创建。
重要: 创建设备文件需要适当的权限(通常是 root 权限),因为这涉及到与底层硬件和内核的交互。
2. 函数原型 Link to heading
#include <sys/stat.h> // 必需 (包含 mode_t, dev_t 等)
#include <sys/sysmacros.h> // 必需 (包含 makedev 等宏,glibc 2.26+)
#include <unistd.h> // 必需
int mknod(const char *pathname, mode_t mode, dev_t dev);
3. 功能 Link to heading
- 创建特殊文件: 根据
mode
参数指定的类型,在文件系统中创建一个名为pathname
的特殊文件节点。 - 指定设备信息: 对于设备文件,使用
dev
参数指定主设备号 (major number) 和次设备号 (minor number),这两个号码唯一标识了系统中的一个硬件设备或内核驱动。
4. 参数 Link to heading
const char *pathname
: 指向一个以空字符 (\0
) 结尾的字符串,该字符串包含了要创建的特殊文件的路径名。这可以是相对路径或绝对路径。mode_t mode
: 指定要创建的文件类型和权限。- 文件类型 (必须指定以下之一):
S_IFCHR
: 创建字符设备文件。S_IFBLK
: 创建块设备文件。S_IFIFO
: 创建 FIFO (命名管道)。S_IFSOCK
: 创建 Unix 域套接字(在支持的系统上)。
- 文件权限: 与
chmod
类似,可以使用八进制数(如0666
)或符号常量(如S_IRUSR | S_IWUSR
)来指定文件的访问权限。常见的权限包括0666
(所有用户读写) 或0644
(所有者读写,组和其他用户只读)。 - 组合示例:
S_IFCHR | 0666
: 创建权限为 0666 的字符设备文件。S_IFIFO | 0644
: 创建权限为 0644 的 FIFO。
- 文件类型 (必须指定以下之一):
dev_t dev
: 指定设备号。这个参数对于 FIFO 和套接字会被忽略。- 对于字符设备和块设备: 这个参数是必需的,它包含了主设备号和次设备号。
- 创建
dev_t
: 通常使用<sys/sysmacros.h>
中的makedev(major, minor)
宏来从主设备号 (major
) 和次设备号 (minor
) 创建dev_t
值。- 主设备号 (Major Number): 标识设备的类型或驱动程序。例如,IDE 磁盘的主设备号通常是 3。
- 次设备号 (Minor Number): 标识特定类型下的具体设备实例。例如,
/dev/hda
(主设备号 3, 次设备号 0) 和/dev/hdb
(主设备号 3, 次设备号 64)。
- 获取设备号: 可以使用
ls -l
命令查看现有设备文件的主次设备号。例如,ls -l /dev/null
会显示crw-rw-rw- 1 root root 1, 3 ...
,其中1
是主设备号,3
是次设备号。
5. 返回值 Link to heading
- 成功时: 返回 0。
- 失败时:
- 返回 -1,并设置全局变量
errno
来指示具体的错误原因:EACCES
: 搜索路径名中的某个目录被拒绝,或父目录不支持写入。EDQUOT
: 用户的磁盘配额已满。EEXIST
:pathname
指定的文件已存在。EFAULT
:pathname
指针指向进程地址空间之外。EINVAL
:mode
参数无效(例如,没有指定有效的文件类型位)。ELOOP
: 解析pathname
时遇到符号链接环。ENAMETOOLONG
: 路径名过长。ENOENT
: 路径名前缀中的某个目录不存在。ENOMEM
: 内核内存不足。ENOSPC
: 设备上没有空间来创建新的目录项。ENOTDIR
: 路径名前缀不是一个目录。EPERM
: 调用进程没有权限创建特殊文件。这是最常见的错误,因为创建设备文件通常需要 root 权限。EROFS
: 路径名存在于只读文件系统上。
- 返回 -1,并设置全局变量
6. 相似函数,或关联函数 Link to heading
mkfifo(const char *pathname, mode_t mode)
: 专门用于创建 FIFO (命名管道) 的简化函数。它等价于mknod(pathname, S_IFIFO | mode, 0)
。mkdir(const char *pathname, mode_t mode)
: 用于创建目录。creat(const char *pathname, mode_t mode)
: 用于创建普通文件(已废弃,推荐使用open()
)。stat
,lstat
: 用于检查文件状态,包括确定文件是否为特殊文件以及获取其设备号。major(dev_t dev)
,minor(dev_t dev)
: 用于从dev_t
值中提取主设备号和次设备号。
7. 示例代码 Link to heading
示例 1:创建 FIFO (命名管道) Link to heading
这个例子演示了如何使用 mknod
创建一个 FIFO,并展示其基本用法。
#include <sys/stat.h>
#include <sys/sysmacros.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>
#include <sys/wait.h>
// 使用 mknod 创建 FIFO
int create_fifo_with_mknod(const char *pathname, mode_t mode) {
printf("使用 mknod 创建 FIFO: %s\n", pathname);
// S_IFIFO 表示创建 FIFO
// dev 参数对于 FIFO 被忽略,通常设为 0
if (mknod(pathname, S_IFIFO | mode, 0) == -1) {
perror("mknod 创建 FIFO 失败");
return -1;
}
printf("成功创建 FIFO: %s\n", pathname);
return 0;
}
// 使用 mkfifo 创建 FIFO (推荐方式)
int create_fifo_with_mkfifo(const char *pathname, mode_t mode) {
printf("使用 mkfifo 创建 FIFO: %s\n", pathname);
if (mkfifo(pathname, mode) == -1) {
perror("mkfifo 创建 FIFO 失败");
return -1;
}
printf("成功创建 FIFO: %s\n", pathname);
return 0;
}
// FIFO 写入端
void fifo_writer(const char *fifo_path) {
int fd;
const char *message = "Hello from FIFO writer!";
printf("FIFO 写入端启动...\n");
// 以只写模式打开 FIFO (会阻塞直到有读取端打开)
fd = open(fifo_path, O_WRONLY);
if (fd == -1) {
perror("打开 FIFO 写入端失败");
return;
}
printf("FIFO 写入端已连接\n");
// 写入数据
if (write(fd, message, strlen(message)) == -1) {
perror("写入 FIFO 失败");
} else {
printf("成功写入消息: %s\n", message);
}
close(fd);
printf("FIFO 写入端关闭\n");
}
// FIFO 读取端
void fifo_reader(const char *fifo_path) {
int fd;
char buffer[256];
ssize_t bytes_read;
printf("FIFO 读取端启动...\n");
// 以只读模式打开 FIFO (会阻塞直到有写入端打开)
fd = open(fifo_path, O_RDONLY);
if (fd == -1) {
perror("打开 FIFO 读取端失败");
return;
}
printf("FIFO 读取端已连接\n");
// 读取数据
bytes_read = read(fd, buffer, sizeof(buffer) - 1);
if (bytes_read == -1) {
perror("读取 FIFO 失败");
} else {
buffer[bytes_read] = '\0';
printf("成功读取消息: %s\n", buffer);
}
close(fd);
printf("FIFO 读取端关闭\n");
}
int main() {
const char *fifo_path = "test_fifo";
printf("=== FIFO (命名管道) 创建和使用演示 ===\n");
// 1. 使用 mknod 创建 FIFO
if (create_fifo_with_mknod(fifo_path, 0666) == -1) {
// 如果失败,尝试使用 mkfifo (推荐)
if (create_fifo_with_mkfifo(fifo_path, 0666) == -1) {
exit(EXIT_FAILURE);
}
}
// 2. 创建子进程进行通信演示
pid_t pid = fork();
if (pid == -1) {
perror("fork 失败");
unlink(fifo_path);
exit(EXIT_FAILURE);
} else if (pid == 0) {
// 子进程作为写入端
sleep(1); // 让父进程先打开读取端
fifo_writer(fifo_path);
exit(EXIT_SUCCESS);
} else {
// 父进程作为读取端
fifo_reader(fifo_path);
// 等待子进程结束
wait(NULL);
// 清理 FIFO 文件
if (unlink(fifo_path) == -1) {
perror("删除 FIFO 失败");
} else {
printf("已删除 FIFO: %s\n", fifo_path);
}
}
return 0;
}
代码解释:
- 定义了两个创建 FIFO 的函数:
create_fifo_with_mknod
: 使用mknod
创建 FIFO,展示了S_IFIFO
标志的使用。create_fifo_with_mkfifo
: 使用专门的mkfifo
函数,这是创建 FIFO 的推荐方式。
fifo_writer
函数模拟 FIFO 的写入端,以O_WRONLY
模式打开 FIFO 并写入消息。fifo_reader
函数模拟 FIFO 的读取端,以O_RDONLY
模式打开 FIFO 并读取消息。main
函数首先创建 FIFO,然后使用fork
创建子进程。父进程作为读取端,子进程作为写入端,演示了 FIFO 的基本通信机制。- 最后通过
unlink
删除 FIFO 文件。
示例 2:创建字符设备和块设备文件 Link to heading
这个例子演示了如何使用 mknod
创建字符设备和块设备文件。注意:这需要 root 权限,并且需要知道正确的主次设备号。
#define _GNU_SOURCE
#include <sys/stat.h>
#include <sys/sysmacros.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
// 打印设备文件信息
void print_device_info(const char *pathname) {
struct stat sb;
if (stat(pathname, &sb) == -1) {
perror("stat");
return;
}
if (S_ISCHR(sb.st_mode)) {
printf("字符设备文件: %s\n", pathname);
printf(" 主设备号: %d\n", major(sb.st_rdev));
printf(" 次设备号: %d\n", minor(sb.st_rdev));
printf(" 权限: %04o\n", sb.st_mode & 0777);
} else if (S_ISBLK(sb.st_mode)) {
printf("块设备文件: %s\n", pathname);
printf(" 主设备号: %d\n", major(sb.st_rdev));
printf(" 次设备号: %d\n", minor(sb.st_rdev));
printf(" 权限: %04o\n", sb.st_mode & 0777);
} else {
printf("不是设备文件: %s\n", pathname);
}
}
// 创建字符设备文件
int create_char_device(const char *pathname, int major_num, int minor_num, mode_t mode) {
dev_t dev = makedev(major_num, minor_num);
printf("创建字符设备文件: %s\n", pathname);
printf(" 主设备号: %d\n", major_num);
printf(" 次设备号: %d\n", minor_num);
printf(" 权限: %04o\n", mode);
// S_IFCHR 表示创建字符设备文件
if (mknod(pathname, S_IFCHR | mode, dev) == -1) {
printf("创建失败: %s\n", strerror(errno));
if (errno == EPERM) {
printf(" 权限不足。创建设备文件通常需要 root 权限。\n");
}
return -1;
}
printf("成功创建字符设备文件\n");
print_device_info(pathname);
return 0;
}
// 创建块设备文件
int create_block_device(const char *pathname, int major_num, int minor_num, mode_t mode) {
dev_t dev = makedev(major_num, minor_num);
printf("创建块设备文件: %s\n", pathname);
printf(" 主设备号: %d\n", major_num);
printf(" 次设备号: %d\n", minor_num);
printf(" 权限: %04o\n", mode);
// S_IFBLK 表示创建块设备文件
if (mknod(pathname, S_IFBLK | mode, dev) == -1) {
printf("创建失败: %s\n", strerror(errno));
if (errno == EPERM) {
printf(" 权限不足。创建设备文件通常需要 root 权限。\n");
}
return -1;
}
printf("成功创建块设备文件\n");
print_device_info(pathname);
return 0;
}
// 演示常见设备的主次设备号
void show_common_devices() {
printf("\n=== 常见设备的主次设备号 ===\n");
printf("注意: 这些信息可能因系统和内核版本而异\n");
printf("字符设备:\n");
printf(" /dev/null - 主: 1, 次: 3\n");
printf(" /dev/zero - 主: 1, 次: 5\n");
printf(" /dev/random - 主: 1, 次: 8\n");
printf(" /dev/urandom - 主: 1, 次: 9\n");
printf(" /dev/tty - 主: 5, 次: 0\n");
printf(" /dev/console - 主: 5, 次: 1\n");
printf(" /dev/ptmx - 主: 5, 次: 2\n");
printf("\n块设备:\n");
printf(" /dev/ram0 - 主: 1, 次: 0 (RAM disk)\n");
printf(" /dev/loop0 - 主: 7, 次: 0 (回环设备)\n");
printf(" /dev/fd0 - 主: 2, 次: 0 (软盘)\n");
printf(" /dev/sda - 主: 8, 次: 0 (第一块 SCSI/SATA 硬盘)\n");
printf(" /dev/sda1 - 主: 8, 次: 1 (第一块硬盘的第一个分区)\n");
printf("\n");
}
int main(int argc, char *argv[]) {
printf("=== 设备文件创建演示 ===\n");
printf("当前用户 UID: %d\n", getuid());
printf("当前用户 EUID: %d\n", geteuid());
if (geteuid() != 0) {
printf("警告: 创建设备文件通常需要 root 权限。\n");
printf("以下操作可能会失败。\n\n");
}
show_common_devices();
// 演示创建一些常见的虚拟设备文件
printf("=== 创建测试设备文件 ===\n");
// 1. 创建 /dev/null 的副本 (字符设备)
printf("1. 尝试创建字符设备 (类似 /dev/null)\n");
create_char_device("test_null", 1, 3, 0666);
// 2. 创建 /dev/zero 的副本 (字符设备)
printf("\n2. 尝试创建字符设备 (类似 /dev/zero)\n");
create_char_device("test_zero", 1, 5, 0666);
// 3. 创建 RAM disk 的副本 (块设备)
printf("\n3. 尝试创建块设备 (类似 /dev/ram0)\n");
create_block_device("test_ram", 1, 0, 0660);
// 4. 展示已创建的设备文件信息
printf("\n=== 已创建的设备文件信息 ===\n");
if (access("test_null", F_OK) == 0) {
print_device_info("test_null");
}
if (access("test_zero", F_OK) == 0) {
print_device_info("test_zero");
}
if (access("test_ram", F_OK) == 0) {
print_device_info("test_ram");
}
// 5. 清理测试文件
printf("\n=== 清理测试文件 ===\n");
const char *test_files[] = {"test_null", "test_zero", "test_ram"};
for (int i = 0; i < 3; i++) {
if (access(test_files[i], F_OK) == 0) {
if (unlink(test_files[i]) == -1) {
printf("删除 %s 失败: %s\n", test_files[i], strerror(errno));
} else {
printf("已删除 %s\n", test_files[i]);
}
}
}
printf("\n=== 总结 ===\n");
printf("mknod 使用要点:\n");
printf("1. 创建字符设备: mknod(pathname, S_IFCHR | mode, makedev(major, minor))\n");
printf("2. 创建块设备: mknod(pathname, S_IFBLK | mode, makedev(major, minor))\n");
printf("3. 创建 FIFO: mknod(pathname, S_IFIFO | mode, 0) (推荐使用 mkfifo)\n");
printf("4. 需要 root 权限\n");
printf("5. 需要知道正确的主次设备号\n");
printf("6. 现代系统通常自动管理设备文件\n");
return 0;
}
代码解释:
print_device_info
函数使用stat
和major
/minor
宏来显示设备文件的详细信息。create_char_device
和create_block_device
函数封装了使用mknod
创建字符设备和块设备的逻辑。show_common_devices
函数列出了一些常见设备的主次设备号作为参考。main
函数演示了创建几种虚拟设备文件的过程,并在最后清理创建的文件。- 代码包含了详细的错误处理和权限检查信息。
示例 3:错误处理和权限检查 Link to heading
这个例子重点演示 mknod
可能遇到的各种错误情况及其处理。
#include <sys/stat.h>
#include <sys/sysmacros.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>
// 演示各种 mknod 错误情况
void demonstrate_mknod_errors() {
printf("=== Mknod 错误处理演示 ===\n");
// 1. 权限不足错误 (最常见的错误)
printf("\n1. 权限不足错误 (EPERM)\n");
printf(" 尝试在当前目录创建设备文件...\n");
if (mknod("no_permission_dev", S_IFCHR | 0666, makedev(1, 3)) == -1) {
printf(" 错误: %s\n", strerror(errno));
if (errno == EPERM) {
printf(" 说明: 普通用户通常没有权限创建设备文件\n");
printf(" 解决: 使用 sudo 或以 root 身份运行\n");
}
}
// 2. 文件已存在错误
printf("\n2. 文件已存在错误 (EEXIST)\n");
// 先创建一个普通文件
FILE *fp = fopen("existing_file", "w");
if (fp) {
fprintf(fp, "test");
fclose(fp);
printf(" 先创建文件: existing_file\n");
}
// 尝试用 mknod 覆盖已存在的文件
printf(" 尝试用 mknod 创建同名设备文件...\n");
if (mknod("existing_file", S_IFCHR | 0666, makedev(1, 3)) == -1) {
printf(" 错误: %s\n", strerror(errno));
if (errno == EEXIST) {
printf(" 说明: 文件已存在,mknod 不会覆盖\n");
printf(" 解决: 先删除文件或使用不同的名称\n");
}
}
unlink("existing_file");
// 3. 无效的 mode 参数
printf("\n3. 无效的 mode 参数 (EINVAL)\n");
printf(" 尝试使用无效的文件类型...\n");
// 故意不指定文件类型标志
if (mknod("invalid_mode", 0666, makedev(1, 3)) == -1) {
printf(" 错误: %s\n", strerror(errno));
if (errno == EINVAL) {
printf(" 说明: mode 参数必须包含有效的文件类型标志\n");
printf(" 解决: 使用 S_IFCHR, S_IFBLK, S_IFIFO 等\n");
}
}
// 4. 路径不存在
printf("\n4. 路径不存在 (ENOENT)\n");
printf(" 尝试在不存在的目录中创建文件...\n");
if (mknod("/nonexistent/path/device", S_IFCHR | 0666, makedev(1, 3)) == -1) {
printf(" 错误: %s\n", strerror(errno));
if (errno == ENOENT) {
printf(" 说明: 路径中的某个目录不存在\n");
printf(" 解决: 确保路径中的所有目录都存在\n");
}
}
// 5. 只读文件系统
printf("\n5. 只读文件系统 (EROFS)\n");
printf(" 尝试在 /proc 中创建设备文件...\n");
if (mknod("/proc/test_device", S_IFCHR | 0666, makedev(1, 3)) == -1) {
printf(" 错误: %s\n", strerror(errno));
if (errno == EROFS) {
printf(" 说明: 不能在只读文件系统上创建文件\n");
}
}
// 6. 路径名过长
printf("\n6. 路径名过长 (ENAMETOOLONG)\n");
char long_path[2000];
memset(long_path, 'a', sizeof(long_path) - 1);
long_path[sizeof(long_path) - 1] = '\0';
// 将中间某个位置设为 '/'
long_path[100] = '/';
printf(" 尝试使用过长的路径名...\n");
if (mknod(long_path, S_IFCHR | 0666, makedev(1, 3)) == -1) {
printf(" 错误: %s\n", strerror(errno));
if (errno == ENAMETOOLONG) {
printf(" 说明: 路径名超过了系统限制\n");
}
}
printf("\n");
}
// 演示正确的设备文件使用方式
void demonstrate_proper_usage() {
printf("=== 正确的设备文件使用方式 ===\n");
printf("1. 系统管理场景:\n");
printf(" - 通常由 udev 或 systemd 在设备插拔时自动创建\n");
printf(" - 系统安装时由安装脚本创建\n");
printf(" - 手动创建时需要 root 权限和正确的设备号\n\n");
printf("2. 开发测试场景:\n");
printf(" - 创建测试用的 FIFO 进行进程间通信\n");
printf(" - 使用回环设备 (loop device) 挂载磁盘镜像\n");
printf(" - 创建 RAM disk 进行性能测试\n\n");
printf("3. 推荐做法:\n");
printf(" - 对于 FIFO: 优先使用 mkfifo()\n");
printf(" - 对于设备文件: 依赖系统自动管理\n");
printf(" - 手动创建时: 仔细核对主次设备号\n");
printf(" - 权限设置: 遵循最小权限原则\n\n");
}
// 演示设备号操作
void demonstrate_device_numbers() {
printf("=== 设备号操作演示 ===\n");
// 常见设备的设备号
struct {
const char *name;
int major;
int minor;
const char *type;
} devices[] = {
{"null", 1, 3, "字符设备"},
{"zero", 1, 5, "字符设备"},
{"random", 1, 8, "字符设备"},
{"full", 1, 7, "字符设备"},
{"ram0", 1, 0, "块设备"},
{"loop0", 7, 0, "块设备"},
{"sda", 8, 0, "块设备"}
};
printf("常见设备的主次设备号:\n");
for (int i = 0; i < 7; i++) {
dev_t dev = makedev(devices[i].major, devices[i].minor);
printf(" %-8s: 主=%2d, 次=%2d (%s) -> dev_t=0x%lx\n",
devices[i].name, devices[i].major, devices[i].minor,
devices[i].type, (unsigned long)dev);
// 演示从 dev_t 提取主次设备号
int major_num = major(dev);
int minor_num = minor(dev);
printf(" 提取: 主=%2d, 次=%2d\n", major_num, minor_num);
}
printf("\n");
}
int main() {
printf("当前进程信息:\n");
printf(" 实际 UID: %d\n", getuid());
printf(" 有效 UID: %d\n", geteuid());
printf(" 实际 GID: %d\n", getgid());
printf(" 有效 GID: %d\n", getegid());
printf("\n");
demonstrate_mknod_errors();
demonstrate_device_numbers();
demonstrate_proper_usage();
printf("=== 总结 ===\n");
printf("mknod 常见错误类型:\n");
printf(" EPERM: 权限不足(最常见)\n");
printf(" EEXIST: 文件已存在\n");
printf(" EINVAL: 无效的 mode 参数\n");
printf(" ENOENT: 路径不存在\n");
printf(" EROFS: 只读文件系统\n");
printf(" ENAMETOOLONG: 路径名过长\n\n");
printf("使用建议:\n");
printf(" 1. 创建 FIFO 优先使用 mkfifo()\n");
printf(" 2. 创建设备文件需要 root 权限\n");
printf(" 3. 确保使用正确的主次设备号\n");
printf(" 4. 现代系统推荐依赖 udev 自动管理\n");
printf(" 5. 注意错误处理和权限检查\n");
return 0;
}
代码解释:
demonstrate_mknod_errors
函数逐一演示了mknod
可能遇到的各种典型错误。demonstrate_proper_usage
函数总结了mknod
的正确使用场景和推荐做法。demonstrate_device_numbers
函数展示了如何使用makedev
、major
、minor
宏来操作设备号。main
函数协调整个演示过程,并在最后提供总结。
编译和运行:
# 编译示例
gcc -o mknod_example1 mknod_example1.c
gcc -o mknod_example2 mknod_example2.c
gcc -o mknod_example3 mknod_example3.c
# 运行示例 (注意权限要求)
# 示例1: FIFO 演示
./mknod_example1
# 示例2: 设备文件演示 (需要 root 权限)
sudo ./mknod_example2
# 示例3: 错误处理演示
./mknod_example3
总结:
mknod
函数是 Linux 系统编程中用于创建特殊文件节点的重要工具。虽然普通应用程序很少直接使用它,但理解其工作原理对于系统管理、设备驱动开发和底层编程至关重要。在使用时必须注意权限要求、正确的设备号以及适当的错误处理。对于 FIFO,推荐使用更简单的 mkfifo
函数。