Linux C系统调用如何实现与优化?

99ANYc3cd6
预计阅读时长 29 分钟
位置: 首页 C语言 正文

什么是系统调用?

系统调用是用户程序请求操作系统内核提供服务的一种机制

linux c语言 系统调用
(图片来源网络,侵删)

你可以把操作系统内核想象成一个国家最核心的政府部门,它掌握着所有最底层的资源(比如文件、内存、设备、进程等),而你的应用程序(比如一个文本编辑器、一个 Web 服务器)就是一个普通公民,如果你想使用这些核心资源(比如读写一个文件、创建一个进程),你不能直接自己去操作,必须通过一个“官方渠道”向内核提出申请,这个“官方渠道”就是系统调用。

为什么需要系统调用?

  1. 保护性:这是最重要的原因,如果用户程序可以随意访问硬件和内存,它们可能会破坏操作系统的稳定性,导致整个系统崩溃,系统调用提供了一个受控的接口,确保用户程序在内核的监督下安全地操作资源。
  2. 抽象性:系统调用为复杂的硬件操作提供了简单、统一的接口,你不需要关心硬盘的具体型号、扇区大小,只需要调用 read() 函数就能读取数据,内核会帮你处理所有底层细节。
  3. 稳定性:通过统一的接口,内核可以确保所有资源访问都遵循一定的规则,从而维护整个系统的稳定。

系统调用与 C 语言库函数的关系

这是一个非常容易混淆但至关重要的概念。

系统调用是直接进入内核的入口点,是操作系统提供的最原始、最底层的功能。writereadopenfork 等都是系统调用。

linux c语言 系统调用
(图片来源网络,侵删)

C 标准库(如 glibc)是一系列预先编写好的函数集合,它位于用户空间,是连接你的程序和系统调用的“桥梁”。

关系: 很多 C 库函数(如 printf, fopen, malloc)在内部封装了一个或多个系统调用,以提供更方便、更高级的功能。

经典例子:printf

当你调用 printf("Hello, World\n"); 时,背后发生了什么?

linux c语言 系统调用
(图片来源网络,侵删)
  1. printf 函数本身是 C 标准库的一部分,它在用户空间运行。
  2. printf 函数会解析格式字符串,将 "Hello, World\n" 放在一个缓冲区里。
  3. 当缓冲区满了或者遇到换行符 \n 时,printf 会调用一个系统调用——通常是 write
  4. write 系统会陷入内核模式,内核将缓冲区中的数据写入到标准输出(你的终端屏幕)。
  5. 执行完毕后,内核返回到用户空间,printf 函数继续执行,然后返回到你的主程序。
特性 系统调用 C 标准库函数
位置 内核空间 用户空间
功能 提供最底层的资源访问 提供更高级、更便捷的封装
接口 通过 int 0x80syscall 指令陷入内核 直接通过 call 指令调用函数
例子 write, read, open, fork printf, fopen, malloc

你写的 C 程序绝大多数时候是在调用 C 库函数,而 C 库函数在幕后为你完成了系统调用的工作,理解系统调用是成为高级 Linux C 程序员的必经之路。


如何在 C 语言中进行系统调用?

主要有三种方式:

直接通过 syscall 指令(最底层)

这种方式不依赖于任何库,直接使用汇编指令触发系统调用,它的优点是极简,不依赖 glibc,在一些非常底层的环境(如自己写一个 Bootloader)中会用到。

语法: long syscall(long number, ...);

number 是系统调用号, 是传递给系统调用的参数。

示例:exit 系统调用 (系统调用号 1)

#include <linux/unistd.h> // 包含系统调用号的头文件
// 手动定义,以防头文件中没有
#ifndef __NR_exit
#define __NR_exit 1
#endif
int main() {
    // 调用 syscall 指令,系统调用号是 __NR_exit (1)
    // 参数是退出码 0
    syscall(__NR_exit, 0);
    // 这行代码永远不会执行
    return 0;
}

编译和运行:

# 直接编译,不需要链接任何库
gcc -o my_exit my_exit.c
# 运行
./my_exit
# echo $?  # 查看上一个进程的退出码,应该是 0

使用 C 库提供的 syscall 函数(推荐)

glibc 也提供了一个 syscall 函数,它比直接使用汇编指令更可移植,并且参数处理更方便。

语法: long syscall(long number, ...); (和指令同名,但由库提供)

示例:write 系统调用 (系统调用号 1)

#include <sys/syscall.h> // glibc 提供的系统调用号和函数原型
#include <unistd.h>      // 包含 write 的库函数声明,但这里我们不用它
#include <stdio.h>
int main() {
    const char *msg = "Hello from syscall!\n";
    size_t len = 25; // 字符串长度
    // 使用 glibc 的 syscall 函数
    // syscall number for write is __NR_write (通常在 1 左右)
    // 参数: fd (文件描述符), buf (缓冲区), count (大小)
    long ret = syscall(SYS_write, STDOUT_FILENO, msg, len);
    if (ret == -1) {
        perror("syscall write failed");
    }
    return 0;
}

编译和运行:

gcc -o my_write my_write.c
./my_write
# 输出: Hello from syscall!

直接调用封装好的库函数(最常用)

这是 99% 的情况下你应该使用的方式,代码更简洁、可读性更强,glibc 可能会为你做很多优化(例如缓冲 I/O)。

示例:write 库函数

#include <unistd.h> // 包含 write 函数的原型
#include <stdio.h>
int main() {
    const char *msg = "Hello from library function!\n";
    size_t len = 29;
    // 直接调用 write 库函数
    // 参数和系统调用一样
    ssize_t ret = write(STDOUT_FILENO, msg, len);
    if (ret == -1) {
        perror("write failed");
    }
    return 0;
}

编译和运行:

gcc -o my_write_lib my_write_lib.c
./my_write_lib
# 输出: Hello from library function!

你会发现,除了 syscall(SYS_write, ...) 变成了 write(...),其他都一样。glibcwrite 函数内部会为你完成 syscall 的调用。


常用系统调用示例

下面是一些最常用的系统调用,并展示如何用最常用的方式(库函数)来使用它们。

文件 I/O (open, read, write, close)

这是系统调用的经典应用。

示例:复制一个文件

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>      // 包含 open 的标志,如 O_RDONLY
#include <sys/stat.h>   // 包含 open 的模式,如 S_IRUSR
#include <errno.h>      // 包含 errno
#define BUFFER_SIZE 1024
int main(int argc, char *argv[]) {
    if (argc != 3) {
        fprintf(stderr, "Usage: %s <source_file> <dest_file>\n", argv[0]);
        exit(EXIT_FAILURE);
    }
    int src_fd = open(argv[1], O_RDONLY);
    if (src_fd == -1) {
        perror("Error opening source file");
        exit(EXIT_FAILURE);
    }
    // O_WRONLY: 只写, O_CREAT: 如果文件不存在则创建, O_TRUNC: 如果文件已存在则清空
    // S_IRUSR|S_IWUSR: 用户读写权限
    int dest_fd = open(argv[2], O_WRONLY | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR);
    if (dest_fd == -1) {
        perror("Error opening/creating destination file");
        close(src_fd); // 记得关闭已经打开的文件
        exit(EXIT_FAILURE);
    }
    char buffer[BUFFER_SIZE];
    ssize_t bytes_read, bytes_written;
    while ((bytes_read = read(src_fd, buffer, BUFFER_SIZE)) > 0) {
        bytes_written = write(dest_fd, buffer, bytes_read);
        if (bytes_written != bytes_read) {
            perror("Error writing to destination file");
            close(src_fd);
            close(dest_fd);
            exit(EXIT_FAILURE);
        }
    }
    if (bytes_read == -1) {
        perror("Error reading from source file");
    }
    close(src_fd);
    close(dest_fd);
    printf("File copied successfully.\n");
    return 0;
}

编译和运行:

gcc -o my_cp my_cp.c
./my_cp source.txt destination.txt

进程控制 (fork, exec, wait, exit)

fork: 创建一个当前进程的几乎完全相同的副本(子进程)。 exec: 用一个新的程序替换当前进程的映像。 wait: 父进程等待子进程结束。 exit: 终止当前进程。

示例:创建子进程并执行 ls 命令

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h> // wait 函数的头文件
int main() {
    pid_t pid = fork(); // fork 创建子进程
    if (pid == -1) {
        perror("fork failed");
        exit(EXIT_FAILURE);
    }
    if (pid > 0) {
        // 父进程执行的代码
        printf("Parent: Created child with PID %d\n", pid);
        int status;
        wait(&status); // 等待子进程结束
        printf("Parent: Child has exited.\n");
    } else {
        // 子进程执行的代码
        printf("Child: I am the child process (PID: %d)\n", getpid());
        // 用 ls 程序替换当前子进程的映像
        // char *argv[] = {"ls", "-l", NULL};
        // execvp("ls", argv);
        // 一个更简单的例子
        char *argv[] = {"echo", "Hello from child process!", NULL};
        execvp("echo", argv);
        // execvp 成功,它永远不会返回到这里
        // 如果执行到这里,说明 execvp 失败了
        perror("execvp failed");
        exit(EXIT_FAILURE);
    }
    return 0;
}

编译和运行:

gcc -o my_proc my_proc.c
./my_proc

输出:

Parent: Created child with PID 1234
Child: I am the child process (PID: 1234)
Hello from child process!
Parent: Child has exited.

进程间通信 - 管道 (pipe)

管道是 Unix/Linux 中最经典的 IPC 机制,允许有亲缘关系(通常是父子)的进程进行通信。

示例:父进程通过管道向子进程发送数据

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
int main() {
    int pipefd[2];
    pid_t pid;
    char buf[30];
    // 创建管道
    if (pipe(pipefd) == -1) {
        perror("pipe");
        exit(EXIT_FAILURE);
    }
    pid = fork();
    if (pid == -1) {
        perror("fork");
        exit(EXIT_FAILURE);
    }
    if (pid > 0) { // 父进程
        close(pipefd[0]); // 父进程关闭读端
        write(pipefd[1], "Hello, child!", 13);
        close(pipefd[1]); // 写完后关闭写端
        wait(NULL); // 等待子进程结束
    } else { // 子进程
        close(pipefd[1]); // 子进程关闭写端
        read(pipefd[0], buf, sizeof(buf));
        printf("Child received: %s\n", buf);
        close(pipefd[0]); // 读取完后关闭读端
    }
    return 0;
}

编译和运行:

gcc -o my_pipe my_pipe.c
./my_pipe

输出:

Child received: Hello, child!

  1. 核心概念:系统调用是用户程序请求内核服务的唯一途径,用于保护系统资源和提供抽象。
  2. 三种方式
    • syscall 指令:最底层,不依赖库,适用于极端环境。
    • glibcsyscall 函数:比指令更方便,但仍是直接调用。
    • C 库函数:最常用、最推荐的方式,提供了高级封装和更好的可移植性。
  3. 常用系统调用
    • 文件 I/O: open, read, write, close, lseek
    • 进程控制: fork, exec系列 (execl, execvp等), wait, exit
    • 进程间通信: pipe, mkfifo (命名管道), shm_open (共享内存)。
    • 信息获取: getpid, getppid, getuid, getgid
  4. 错误处理:系统调用在失败时通常会返回 -1,并设置一个全局变量 errno 来表示具体的错误类型,使用 perror() 函数可以打印出有意义的错误信息。

深入理解系统调用是掌握 Linux C 编程的基石,当你遇到性能瓶颈或者需要实现一些库函数无法提供的特殊功能时,直接使用系统调用就会变得非常必要。

-- 展开阅读全文 --
头像
sober算子C语言代码如何实现?
« 上一篇 02-08
dede5.7图片集模板如何制作调用?
下一篇 » 02-08
取消
微信二维码
支付宝二维码

目录[+]