Linux 系统调用

1. 系统调用概述

当前操作系统提供给用户操作系统相关资源的【接口】

  • SDK : Soft Development Kits 软件开发工具集
  • API : Application Program Interface ,软件开发接口文档,也是函数说明文档。

系统提供针对于系统资源的使用,处理方式,系统资源包括文件,内存,进程,线程,网络资源。。。

可以根据图片分析,系统调用(System Calls) 是对当前系统资源的【隔离和保护机制】。用户可以通过 Application, Shell ,库函数(Library Routines) 操作系统调用相关函数,对内核资源进行合理的调配使用。

2. 案例说明系统调用和库函数调用区别

例如文件操作

  • 利用 C 语言库函数方式打开文件使用的函数是 FILE *fopen(const char * filepath, const char * mode),如果文件存在得到的是一个针对于当前普通文件的 FILE 类型指针
  • mode 模式选择对应的有 r w a +,实际上是利用系统调用函数 int open(const char * filepath, int flags) , flags 参数对应就是当前文件的打开方式,返回值类型是 int 类型,对应当前文件的【文件描述符】

用户通过 fopen 打开文件,fopen 利用 open 函数对文件资源进行管理操作,可以认为

  • fopen 是餐厅的服务员【Application Shell 库函数】
  • open 是后厨 【系统调用 System Calls】

库函数和系统函数实际上是相辅相成,尤其是系统函数是提供给库函数相关资源的底层实现。

3. 用户态和内核态

库函数(调用系统调用),例如 printf,fopen,fread ,fwrite。。。

库函数(不调用系统调用),strcpy strcat strcmp...

用户态对应的是 C/C++ 提供的库函数和系统调用函数

内核态是系统调用函数通过系统对应预留接口,利用内核对系统资源的管理和提供。

库函数没有重载,而系统函数有重载

4. C语言中 IO 操作的底层实现

核心内容

  • 每一个文件都有一个唯一的【文件描述符 int 类型】
  • 操作文件内容需要利用到文件内容指针
  • 操作文件,系统会提供对应的缓冲区,降低程序频繁操作硬盘的时间,提升效率

5. 文件描述符【重点,必会点】

5.1 概述

【重点】在 Linux 系统中,万物皆为文件。任何一个设备对于当前计算机系统而言,都是一个文件,例如键盘,鼠标,屏幕,都是以文件方式在系统中执行相关操作,同时利用 read write 对设备文件进行操作,从而满足对应的功能实现。

任何一个文件对于当前程序在使用时,都会有一个唯一的对应的【文件描述符 File Descriptor fd】,操作系统和当前程序通过文件描述符对当前文件进行相关操作。可以完成 Read 和 Write 操作。

5.2 程序对应的文件描述符
  • 程序编写

  • 执行程序,利用 ps -A | grep a.out 搜索目标可执行程序对应的 PID 【进程号】

  • 切换到目标路径 /proc 对应当前系统执行进程的资源文件夹,根据对应 PID 找到目标程序对应文件夹

  • 当前程序占用的文件描述符

5.3 任何程序默认占用的文件描述符
  • 0 #define STDIN_FILENO 0 标准输入文件描述符
  • 1 #define STDOUT_FILENO 1 标准输出文件描述符
  • 2 #define STDERR_FILENO 2 标准错误文件描述符

任何一个程序,默认的文件描述符范围是 0 ~ 1023, 已经被系统占用的默认描述符是0 1 2,当前程序打开后续的资源/文件,【对应的最小文件描述符是 3】。

文件在第一次被调用时会分配文件描述符,其他文件再次调用会查看此文件句柄,如果发现已被占用则无法以诸如写操作打开此文件

程序管理文件描述的方式是**【位图】**

当前系统默认进程支持的最大fd个数

07-当前系统默认进程支持的最大fd个数.png

5.4 系统管理程序文件描述符的方式

采用方式为【位图】

08-文件描述符位图分析.png

系统PCB管理进程和进程对应文件描述符

09-系统PCB管理进程和进程对应文件描述符.png

6. 系统调用(System Calls) IO 函数

6.1 打开文件 open

函数文档

  • 读取,写入文件内容,并且文件存在,使用两个参数的 open system calls 函数
  • 如果文件不存在,需要创建,必须使用三个参数的 open system calls 函数
// 所需头文件如下
#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:这是一个指向要打开或创建的文件路径名的字符串指针。它可以是绝对路径(如 /home/user/test.txt),也可以是相对路径(如 test.txt)。

  • flags:该参数指定了文件的打开方式,它是一个位掩码,可以使用多个标志进行按位或(|)组合。以下是一些常用的标志:

    • O_RDONLY:以只读模式打开文件。
    • O_WRONLY:以只写模式打开文件。
    • O_RDWR:以读写模式打开文件。
    • O_CREAT:如果文件不存在,则创建该文件。当使用这个标志时,需要提供第三个参数 mode 来指定新文件的权限。
    • O_EXCL:与 O_CREAT 一起使用时,如果文件已经存在,则 open 调用会失败,返回 -1 并设置 errnoEEXIST。这可以用于确保创建一个新文件。
    • O_TRUNC:如果文件存在且以写模式打开,则将文件截断为零长度。
    • O_APPEND:以追加模式打开文件,每次写操作都将数据追加到文件末尾。
  • mode当使用 O_CREAT 标志时,需要提供这个参数来指定新文件的权限。权限是通过位掩码来设置的,可以使用一些预定义的常量,如:

    • S_IRUSR:用户可读。0400
    • S_IWUSR:用户可写。0200
    • S_IXUSR:用户可执行。0100
    • S_IRWXU : 用户可读可写可执行 0700
    • S_IRGRP:组可读。0040
    • S_IWGRP:组可写。0020
    • S_IXGRP:组可执行。0010
    • S_IRWXG : 组可读可写可执行 0070
    • S_IROTH:其他用户可读。0004
    • S_IWOTH:其他用户可写。0002
    • S_IXOTH:其他用户可执行。0001
    • S_IRWXO : 其他可读可写可执行 0007
    • 以上 mode_t 对应的系统设定相关参数,仅作为参考,实际开发中使用直接利用 8 进制数据描述新建文件权限内容 , 例如 0664 0775

内核态中的文件和文件夹在打开时设置的权限存在系统降级

返回值类型:

  • 返回是当前对应路径文件成功打开之后的【文件描述符 fd File Descriptor】,文件描述符最小值 3
  • 如果打开文件失败,返回 -1
6.2 关闭文件 close

函数文档

#include <unistd.h>

int close(int fd);

参数解释:

  • fd : 对应当前打开文件的【文件描述符 FD File Descriptor】

返回值类型

  • 关闭成功返回 0
  • 关闭失败,返回 -1,并且设置 errno
6.3 案例代码

推荐利用终端命令搜索对应程序的进程号,在 /proc/xxx 中查看 fd 文件描述符打开和关闭的情况

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>

int main(int argc, char const *argv[])
{
    /*
    int open(const char *pathname, int flags);
    int open(const char *pathname, int flags, mode_t mode);

    int close(int fd);
    */

    // 利用 open 函数打开已存在的目标文件,打开方式为只读
    int fd = open("./1.txt", O_RDONLY);

    // 判断是否打开成功
    if (-1 == fd)
    {
        // perror 打印错误信息
        perror("Open target File Failed!");
    }

    // 展示 FD File Descriptor 文件描述符
    printf("fd : %d\n", fd);

    // 利用 open 函数创建一个新的文件
    /*
    当前创建文件要求新文件的权限为 0666 权限,但是创建完成之后,发现当前的
    文件对应的权限为 0664 权限
  
    【系统降级】 Linux 系统会根据当前系统的默认要求,对用户申请创建的文件,进行
    权限降级操作,文件夹默认权限为 0775 而普通文件默认权限为 0664,后续可以通过
    其他操作来修改文件的权限操作。
    */
    int fd1 = open("./2.txt", O_CREAT, 0666);

    if (-1 == fd1)
    {
        // perror 打印错误信息
        perror("Open target File Failed!");
    }

    // 展示 FD File Descriptor 文件描述符
    printf("fd1 : %d\n", fd1);

    // 资源打开,需要关闭操作
    int num = 0;
    scanf("%d", &num);

    close(fd);

    scanf("%d", &num);

    close(fd1);

    scanf("%d", &num);

    return 0;
}
6.4 写入数据到文件 write

函数文档

#include <unistd.h>

ssize_t write(int fd, const void *buf, size_t count);

**【注意】要求在使用 open 函数打开目标文件得到对应的文件描述符时,提供给当前文件的打开 flags 中必须有写入文件的权限,要求参数为 **O_WRONLY O_RDWR O_TRUNC O_APPEND

**参数解释: **

fd : 当前写入文件的文件描述符

buf : 写入数据的缓冲数据空间首地址

count : 写入到当前文件的字节个数

返回值

  • 写入数据成功,返回值是实际写入到文件中的字节个数
  • 写入失败,返回 -1

案例代码

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>

int main(int argc, char const *argv[])
{
    /*
    打开文件,得到对应的文件描述符,同时设置打开方式为 O_WRONLY | O_TRUNC
    */
    int fd = open("./2.txt", O_WRONLY | O_TRUNC);

    if (-1 == fd)
    {
        perror("Open File Failed!");
    }

    printf("当前文件对应的 fd : %d\n", fd);

#if 0
    int num = 10;    
    int ret = write(fd, &num, 4);
#endif
    char * str = "欢送研究僧!";
    /*
    当前数据限制写入字节个数为 100 个字节,当前程序会从指定数据的起始位置
    开始写入数据到文件中,不会因为字符串终止标记 \0 结束当前写入操作
    要求一般情况下,使用 write 函数,数据是多少,就写入多少。
    int ret = write(fd, str, 100);
    */
    int ret = write(fd, str, sizeof("欢送研究僧!"));

    if (-1 == ret)
    {
        perror("Write Data Failed!");
    }

    printf("ret : %d\n", ret);

    close(fd);

    return 0;
}
6.5 从文件中读取数据 read

函数文档

#include <unistd.h>

ssize_t read(int fd, void *buf, size_t count);

**参数解释: **

fd : 读取操作对应的文件描述符

buf : 读取数据临时存储的目标缓冲区内存空间,要求缓冲区空间必须连续,可以是数组,也可以是动态内存申请的空间,通常情况下当前空间采用的数据类型为 char 类型,因为 char 类型占用内存空间为 1 个字节,空间操作灵活度更高

count : 要求读取的字节个数,要求 buf 缓冲区字节个数 大于等于 count

返回值类型

  • 成功:返回实际读取的字节数。若返回值为 0,表示已经到达文件末尾(EOF)。实际读取的字节数可能小于 count,比如遇到文件末尾、被信号中断或者设备一次只能提供部分数据等情况。
  • 失败:返回 -1,并设置 errno 来表明具体的错误类型。

**常见的 **errno

  • EBADFfd 不是一个有效的文件描述符,或者该文件描述符没有以可读模式打开。
  • EFAULTbuf 指向的内存区域不可访问。
  • EINTR:在读取过程中被信号中断。
  • EIO:发生输入/输出错误。
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define DEFAULT_BUFFER_SIZE 64

int main(int argc, char const *argv[])
{
    // 打开目标文件,得到对应的文件描述符,同时设定当前打开方式为 O_RDONLY 
    int fd = open("./1.txt", O_RDONLY);

    if (-1 == fd)
    {
        perror("Open File Failed!");
    }

    // 申请必要的内存空间,同时进行 memset 擦除操作
    char * buf = (char *)malloc(DEFAULT_BUFFER_SIZE);
    memset(buf, 0, DEFAULT_BUFFER_SIZE);

    ssize_t length = read(fd, buf, 64);

    if (0 == length)
    {
        perror("EOF");
    }
    else if (-1 == length)
    {
        perror("Read File Failed!");
    }
    printf("length : %ld\n", length);
    printf("buf : %s\n", buf);

    free(buf);
    close(fd);
    
    return 0;
}
6.6 使用系统调用完成 cp 命令

功能要求

  • 终端执行效果为 ./cp 1.txt ./a,将 1.txt 内容复制到 a 文件夹中,最终效果为在 a 文件夹中存在一个 1.txt 文件,内容和上层文件内容一致。
  • 利用 main 函数中的 argc 和 argv 参数。如果按照以上要求提供的情况
    argc = 3
    argv = {"./cp", "1.txt", "./a"}
    
  • 利用 read 和 write 系统调用函数对当前源文件和目标文件进行读取和写入操作。
  • open 函数需要根据当前文件特征,选择文件的打开方式和文件权限内容