系统调用简介和实现
什么是系统调用(system call)
系统调用,顾名思义,说的是操作系统提供给用户程序调用的一组特殊接口,用户程序可以通过这个特殊接口来获得操作系统内核提供的服务
alt text
系统调用的实现
系统调用是属于操作系统内核的一部分,必须以某种方式提供给进程让它们去调用,相应的操作系统也有不同的运行级别–用户态和内核态,内核态可以毫无限制的访问各种资源,而用户态下的用户进程的各种操作都有限制,显然,
属于内核的系统调用是运行在内核态下,那么如何切换到内核态呢?
答案是软件中断(trap),操作系统一般是通过软件中断从用户态切换到内核态
系统调用和库函数的区别
系统调用是操作系统内核提供的接口,用户程序通过这些接口来请求内核提供的服务,而库函数是编程语言提供的一组预定义函数,这些函数封装了一些常用的操作,简化了编程工作
库函数主要由两类函数组成: 1. 不需要调用系统调用
不需要切换到内核空间即可完成函数全部功能,如strcpy、bzero等字符串操作函数
- 需要调用系统调用
需要切换到内核空间,这类函数通过封装系统调用去实现相应的功能,如print、fread等
错误处理函数
errno 是一个在 C 语言及其派生语言(如
C++)的标准库中使用的全局变量或宏,用于存储操作系统或库函数在执行过程中遇到的最近一次错误代码(类型为int),
不同的错误码表示不同的含义。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| #include <stdio.h> #include <errno.h>
void example_perror() { FILE *fp; fp = fopen("non_existent_file.txt", "r");
if (fp == NULL) { perror("文件读取失败"); } else { fclose(fp); } }
|
虚拟地址空间
如图是一张 Linux
进程地址空间布局图, 核心划分是内核区与用户区: | 区域 | 地址范围
| 作用与特性 | |————————|————|————————————————————————————————————–| |
内核区 (Kernel Space) | 3G 到 4G | 存放 Linux
内核的代码和数据。它是受保护的,用于运行内核程序,如内存管理、进程管理、设备驱动和
VFS(虚拟文件系统)。用户程序不能直接读写这块区域,否则会触发段错误。 |
| 用户区 (User Space) | 0 到 3G | 存放用户程序的代码和数据。这是
int main(…)
函数及其代码、数据、堆栈等执行的地方。用户程序的大部分操作都在这个区域内进行。
|
下面我们再介绍用户区内存段(ELF 文件加载区):
用户区从底部(低地址 0)向上依次排列着由 ELF(Executable and Linkable
Format,可执行文件格式)文件加载而来的各个数据段:
- 代码和数据段 (Code & Data)
这三个段通常是静态分配的,它们的大小在程序编译时就已经确定:
- .text
(代码段):存放程序的机器指令(二进制机器指令)。该区域是只读的,以防止程序意外修改自己的代码。
- 图中标注:受保护的地址
(0∼4K),这是为了捕获对空指针的解引用操作,防止低地址被使用。
- .rodata / .data
(已初始化全局变量):存放程序中已初始化的全局变量和静态变量。
- .bss
(未初始化全局变量):存放程序中未初始化的全局变量和静态变量。在程序加载时,这些变量会被清零处理。
这几个段共同构成了程序的基本静态结构,其中代码段的只读保护是程序安全的重要保障。
- 堆空间 (Heap): 用于程序的动态内存分配(如 C 语言中的 malloc 或 C++
中的 new)。
它从低地址向高地址(图中的向上箭头)增长,其大小在程序运行时动态变化。并且堆空间通常是用户区中最大的一块区域。
- 共享库/动态库 (Shared Libraries/Dynamic Libraries):
存放程序运行时需要链接的共享库文件(如 libc.so - C
标准库)。
多个进程可以共享同一份库的代码和数据,从而节省物理内存。共享库通过系统调用(如
execve)或动态加载器,将 C 标准库、Linux 系统 I/O
函数等加载到这里,供用户代码调用。
而静态库的代码则直接链接到程序的代码段中, 存储在.text 段内。
- 栈空间 (Stack):
用于存放函数调用时的局部变量、函数参数、返回地址等。
它从高地址向低地址(图中的向下箭头)增长。栈空间相对较小,一旦耗尽会导致栈溢出(Stack
Overflow)。
堆和栈的对向增长(一个向上,一个向下)机制可以有效地利用中间的虚拟地址空间,并在它们相遇之前,为程序提供了最大的灵活性。
- 命令行参数和环境变量: 位于用户区最高端,存放着程序启动时传递给 main
函数的参数(如 argc,argv[…])和系统的环境变量(env)。
文件描述符
如图展示了 Linux
进程如何通过文件描述符 (File Descriptor, FD) 机制来管理和访问文件及 I/O
资源。
文件描述符的本质是一个非负整数(通常是 0 到 1023
之间),它在进程的上下文中,是用来唯一标识一个打开的文件、套接字(socket)
或其他 I/O 资源(本质上它们都是文件)的索引。
右图的文件描述符表存储在PCB中, PCB(process control
block)是每个 Linux
进程都有的进程控制块,它位于内核区(Linux kernel)内,
包含了一个指向该进程文件描述符表的指针。
| 文件描述符 (FD) |
名称 |
对应 I/O 资源 |
默认用途 |
| 0 |
STDIN_FILENO |
标准输入 |
默认为只读模式,通常指向键盘输入或文件重定向。 |
| 1 |
STDOUT_FILENO |
标准输出 |
默认为只写模式,通常指向终端显示或文件重定向。 |
| 2 |
STDERR_FILENO |
标准错误 |
默认为只写模式,用于输出程序的错误和诊断信息。 |
| 3 及以上 |
正常文件 I/O |
文件、套接字、管道等 |
用于进程通过 open()、socket() 等系统调用打开的资源。 |
如图所示, FD 0、FD 1、FD 2 是所有进程默认打开的三个标准流.
而当程序调用 open() 或 socket() 等系统调用(进入内核态)打开一个新的 I/O
资源时,内核会在PCB中查找该进程的文件描述符表。它会从 3
开始,寻找最小且当前未被占用的整数作为新的文件描述符分配给该资源。
简而言之,用户进程不直接操作文件,而是操作文件描述符,由内核负责将这个数字与实际的
I/O 资源关联起来
常用文件IO函数
open函数
1 2 3 4 5 6 7 8 9 10 11 12 13
| #include <sys/types.h> #include <sys.stat.h> #include <fcntl.h>
int open(const char *pathname, int flags); int open(const char *pathname, int flags, mode_t mode); 参数: pathname:文件的路径及文件名 flags:打开文件的行为标识(只读、只写、可读可写) mode:这个参数只有在文件不存在时有效,指新建文件时指定文件的权限(数字指定, 如 0644代表rw-r--r--) 返回值: 成功:返回打开的文件描述符 失败:-1
|
其中flags参数常用的取值有: 必选项 (Access Modes) |
取值 (Flag) | 含义 (Meaning) | |————-|———————–| | O_RDONLY |
以只读的方式打开 | | O_WRONLY | 以只写的方式打开 | | O_RDWR |
以可读、可写的方式打开 |
可选项 (Creation/Status Flags):
可以和必选项按位或(使用 | 运算符)起来使用。 | 取值
(Flag) | 含义 (Meaning) | |————-|———————–| | O_CREAT |
文件不存在则创建文件,使用此选项时需使用 mode 说明文件的权限 | | O_EXCL
| 如果同时指定了 O_CREAT,且文件已经存在,则出错 | | O_TRUNC |
如果文件存在,则清空文件内容 | | O_APPEND | 写文件时,数据添加到文件末尾
| | O_NONBLOCK | 对于设备文件,以 O_NONBLOCK 方式打开可以做非阻塞 I/O
|
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
| int main(void) { int fd = -1;
fd = open("txt", O_RDONLY);
/fd = open("txt", O_WRONLY | O_CREAT, 0644);
fd = open("txt", O_WRONLY | O_CREAT | O_EXCL, 0644);
fd = open("txt", O_RDWR | O_CREAT, 0644);
fd = open("txt", O_WRONLY | O_TRUNC | O_CREAT, 0644);
fd = open("txt", O_WRONLY | O_APPEND);
close(fd); return 0; }
|
close函数
1 2 3 4 5 6 7 8 9 10
| #include <unistd.h>
int close(int fd); 功能: 关闭已打开的文件 参数: fd:文件描述符,open()的返回值 返回值: 成功:0 失败:-1,并设置errno
|
write函数
1 2 3 4 5 6 7 8 9 10 11
| #include <unistd.h> ssize_t write(int fd, const void *buf, size_t count); 功能: 把指定数目的数据写到文件(fd), 注意 write() 函数会覆盖从当前文件偏移量开始的现有数据(与 lseek() 函数结合使用时尤为重要)。 参数: fd:文件描述符 buf:数据首地址 count:写入数据的长度(字节) 返回值: 成功:实际写入数据的字节个数 失败:-1
|
示例代码:
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
| int main(void) { int fd = -1; int ret = -1; char *str = "hello itcast"; fd = open("txt", O_WRONLY | O_CREAT, 0644); if (-1 == fd) { perror("open"); return 1; } printf("fd = %d\n", fd); ret = write(fd, str, strlen(str)); if (-1 == ret) { perror("write"); return 1; } printf("write len: %d\n", ret); close(fd); return 0; }
|
read函数
1 2 3 4 5 6 7 8 9 10 11 12
| #include <unistd.h>
ssize_t read(int fd, void *buf, size_t count); 功能: 把指定数目的数据读到内存(缓冲区) 参数: fd:文件描述符 buf:内存首地址, 用于存储读取到的数据 count:读取的字节个数 返回值: 成功:实际读取到的字节个数 失败:-1
|
示例代码:
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
| #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <fcntl.h> #include <errno.h>
#define BUF_SIZE 100
int main(void) { int fd = -1; ssize_t ret = -1; char buffer[BUF_SIZE] = {0};
fd = open("data.txt", O_RDONLY); if (-1 == fd) { perror("open:"); return 1; } printf("成功打开文件,文件描述符 (FD) 为: %d\n", fd); ret = read(fd, buffer, BUF_SIZE - 1); if (-1 == ret) { perror("read:"); close(fd); return 1; } buffer[ret] = '\0'; printf("读取到的字节长度 (ret): %zd\n", ret); printf("文件内容:\n%s\n", buffer); close(fd); return 0; }
|
lseek函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| #include <sys/types.h> #include <unistd.h> typedef long off_t;
off_t lseek(int fd, off_t offset, int whence); 功能: 改变文件的偏移量, 实现文件的随机访问(这很重要, lseek()本身不进行读写操作,它唯一的作用是修改内核记录的文件偏移量) 参数: fd: 文件描述符 offset: 根据 whence 来移动的位数(偏移量), 可以是正数, 也可以是负数, 如果正数, 则相对于 whence 往右移动, 如果是负数, 则相对于 whence 往左移动。 whence: 其取值如下: SEEK_SET: 从文件开头移动 offset 个字节 SEEK_CUR: 从当前位置移动 offset 个字节 SEEK_END: 从文件末尾移动 offset 个字节 返回值: 若 lseek 成功执行, 返回新的偏移量(绝对偏移量, 即从文件开头算起的字节数) 如果失败, 返回 -1
|
如果 lseek() 将文件偏移量设置到当前文件末尾之后(例如文件长 10
字节,你 lseek 到 100),然后执行 write 操作,文件尺寸会增大到 101
字节。中间跳过的 90 个字节是未初始化的,通常被称为“文件空洞”(File
Hole)。
示例代码:
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
| #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <fcntl.h> #include <string.h>
void lseek_example() { int fd; char buffer[20] = {0}; ssize_t ret;
fd = open("sample.txt", O_RDWR); if (fd == -1) { perror("open sample.txt"); return; } off_t new_offset = lseek(fd, 4, SEEK_SET); if (new_offset == -1) { perror("lseek SEEK_SET failed"); } else { printf("1. SEEK_SET: 成功将偏移量设置为: %ld\n", new_offset); ret = read(fd, buffer, 3); buffer[ret] = '\0'; printf(" 读取内容: %s (从 '4' 开始读 3 个)\n", buffer); } new_offset = lseek(fd, 2, SEEK_CUR); if (new_offset == -1) { perror("lseek SEEK_CUR failed"); } else { printf("2. SEEK_CUR: 成功将偏移量移动到: %ld\n", new_offset); ret = write(fd, "A", 1); printf(" 写入内容: A\n"); }
new_offset = lseek(fd, 0, SEEK_END);
if (new_offset == -1) { perror("lseek SEEK_END failed"); } else { printf("3. SEEK_END: 成功将偏移量移动到文件末尾: %ld\n", new_offset); ret = write(fd, "Z", 1); printf("在文件末尾追加内容: Z\n"); }
close(fd); printf("--- 文件操作完成 ---\n"); }
|
文件描述符复制
在 Linux/Unix 系统编程中,dup() 和 dup2()
是两个重要的系统调用,用于复制(或称为重定向)文件描述符。
dup函数
1 2 3 4 5 6 7 8 9 10
| #include <unistd.h>
int dup(int oldfd); 功能: 用于根据旧的文件描述符复制出一个新的文件描述符, 原始的文件描述符 oldfd 和新的文件描述符都将指向同一个文件 参数: oldfd: 需要复制的文件描述符 返回值: 成功: 返回新的文件描述符 失败: -1
|
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
| int main(void) { int fd = -1; int newfd = -1;
fd = open("txt", O_RDWR | O_CREAT, 0644); if (-1 == fd) { perror("open"); return 1; } printf("fd = %d\n", fd);
newfd = dup(fd); if (-1 == newfd) { perror("dup"); return 1; } printf("newfd = %d\n", newfd);
write(fd, "ABCDEFG", 7); write(newfd, "1234567", 7);
close(fd); close(newfd); return 0; }
|
由于 newfd 和 fd 指向同一个文件,
共享同一文件偏移量,因此这次写入操作将从上一次写入结束的位置开始。也就是说,1234567
会紧接着 ABCDEFG 写入文件,文件内容最终是 ABCDEFG1234567
dup2函数
1 2 3 4 5 6 7 8 9 10
| #include <unistd.h> int dup2(int oldfd, int newfd); 功能: 将 oldfd 复制到 newfd, 并且newfd 的值可以人为指定. 如果 newfd 已经被打开, 则先调用 close() 关闭此描述符,断开它与原文件的关联,然后再使用这个合法的数字作为新的文件描述符。 参数: oldfd: 需要复制的文件描述符 newfd: 目标文件描述符, 这个描述符的值可以指定。 返回值: 成功: 返回新的文件描述符 失败: -1
|
dup2() 最常见的用途是将标准 I/O 流(FD 0、1、2)重定向到文件。
1 2 3 4 5 6 7 8 9 10 11
| int file_fd = open("output.log", O_WRONLY | O_CREAT | O_TRUNC, 0644); if (file_fd == -1) { }
dup2(file_fd, 1);
printf("这条信息现在写入了 output.log 文件中。\n"); close(file_fd);
|
### fcntl函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| #include <unistd.h> #include <fcntl.h>
int fcntl(int fd, int cmd, ... ); 功能: 改变已打开的文件的性质,fcntl 针对文件描述符提供控制功能 参数: fd: 文件描述符 cmd: 控制命令 F_DUPFD: 复制文件描述符,类似于 dup() F_GETFD: 获取文件描述符标志 F_SETFD: 设置文件描述符标志 F_GETFL: 获取文件状态标志和访问模式 F_SETFL: 设置文件状态标志 返回值: 成功: 返回值取决于具体的 cmd 失败: -1
|
目录相关操作