02-系统调用、文件、目录

发布时间:2024年01月09日

系统调用、文件、目录

1. 系统调用

1.1 系统编程概述

  • 操作系统的职责:

    • 操作系统用来管理所有的资源,并将不同的设备和不同的程序关联起来。
  • 什么是 Linux 系统编程

    • 在有操作系统的环境下编程,并使用操作系统提供的系统调用及各种库,对系统资源进行访问。
    • C 语言 + 使用系统调用的方法,就可以进行 Linux 系统编程了。

1.2 系统调用概述

  • 是操作系统提供给用户使其可以 操作内核 提供服务的 一组函数接口

在这里插入图片描述

  • Linux 的不同版本提供了 两三百个系统调用

  • 用户程序可以通过这组接口获得操作系统(内核)提供的服务。

    • 在这里插入图片描述

    • 例如:

      • 用户可以通过文件系统相关的系统调用,请求系统打开文件、关闭文件或读写文件。
  • 系统调用按照功能逻辑大致 可分为:

    • 进程控制、进程间通信、文件系统控制、系统控制、内存管理、网络管理、 socket 控制、用户管理。
  • 系统调用的 返回值

    • 通常,用一个 负的返回值 来表明错误,返回一个 0 值表明成功。错误信息存放在全局变量 errno 中,用户可用 perror 函数打印出错信息。
  • 系统调用遵循的规范

    • 在 Linux 中,应用程序编程接口(API)遵循 POSIX 标准。
    • POSIX 标准基于当时现有的 UNIX实践和经验,描述了操作系统的系统调用编程接口(实际上就是 API),用于保证应用程序可以在源代码一级上在多种操作系统上移植运行。
    • 如:
      • linux 下写的 open、 write 、 read 可以直接移植到 unix 操作系统下。

1.3 用户态和内核态

1.3.1 引入
  • CPU 指令是可以直接操作硬件的,要是因为指令操作的不规范,造成的错误是会影响整个 计算机系统 的。好比你写 一个程序,但是因为你对 硬件操作 不熟悉,出现问题,那么影响范围是多大?是整个计算机系统,操作系统内核、及其其他所有正在运
    行的程序,都会因为你操作失误而受到不可挽回的错误,那么你只有重启整个计算机才行。

  • 而对于 硬件的操作 是非常复杂的,参数众多,出问题的几率相当大,必须及其谨慎的进行操作,这对于个人开发者来说是个艰巨的任务,同时个人开发者在这方面也是不被信任的。所以 操作系统内核 直接屏蔽了个人开发者对于硬件操作 的可能。

  • 这方面 系统内核 对 硬件操作 进行了封装处理,对外提供标准函数库,操作更简单、更安全。

    • 比如 我们要打开一个文件:
      • C标准函数库中对应的是 fopen()
      • 其内部封装的是内核中的系统函数open()
  • 因为这个需求,硬件设备商直接提供了硬件级别的支持,做法就是对 CPU 指令设置了权限,不同级别的权限可以使用的 CPU 指令是有限制的。以 Inter CPU 为例,Inter 把 CPU 指令操作的 权限划为4级

    • ring 0
    • ring 1
    • ring 2
    • ring 3

其中

  • ring 0 权限最高,可以使用所有 CPU 指令
  • ring 3 权限最低,仅能使用常规 CPU 指令,这个级别的权限 不能使用访问硬件资源的指令,比如 IO 读写、网卡访问、申请内存都不行,都没有权限。

Linux 系统内核采用了:ring 0 和 ring 3 这2个权限

  • ring 0:内核态,完全在 操作系统内核 中运行,由专门的 内核线程 在 CPU 中执行其任务

  • ring 3:用户态,在 应用程序 中运行,由 用户线程 在 CPU 中执行其任务

Linux 系统中所有 对硬件资源的操作 都必须 在 内核态 状态下 执行,比如 IO 的读写,网络的操作

1.3.2 区别
  1. 用户态的代码必须由 用户线程 去执行;内核态的代码必须由 内核线程 去执行
  2. 用户态、内核态 或者说 用户线程、内核线程 可以使用的资源是不同的,尤体现在内存资源上。Linux 内核对每一个进程都会分配 4G 虚拟内存空间地址
    • 用户态: —> 只能操作 0-3G 的内存地址
    • 内核态: —> 0-4G 的内存地址都可以操作,尤其是对 3-4G 的高位地址必须由内核态去操作,因为所有进程的 3-4G 的高位地址使用的都是同一块、专门留给 系统内核 使用的 1G 物理内存
  3. 所有对 硬件资源系统内核数据 的访问都必须由 内核态 去执行
1.3.3 如何切换内核态
  • 通过软件中断
1.3.4 软件中断与硬件中断
  • 软件中断
    • 软件中断是由软件程序触发的中断,如系统调用、软中断、异常等。软件中断不是由硬件设备触发的,而是由软件程序主动发起的,一般用于系统调用、进程切换、异常处理等任务。软件中断需要在程序中进行调用,其响应速度和实时性相对较差,但是具有灵活性和可控性高的特点。
    • 如:程序中出现的内存溢出,数组下标越界等。
  • 硬件中断
    • 硬件中断是由硬件设备触发的中断,如时钟中断、串口接收中断、外部中断等。当硬件设备有数据或事件需要处理时,会向CPU发送一个中断请求,CPU在收到中断请求后,会立即暂停当前正在执行的任务,进入中断处理程序中处理中断请求。硬件中断具有实时性强、可靠性高、处理速度快等特点。
    • 如:当点击按钮扫描系统高低电频时等。

1.4 系统调用与库函数的关系

  • 库函数可以调用系统调用提供的接口,也可以不调用系统调用提供的接口
  • 如:
    • 不调用系统调用的库函数:strcpybzero
    • 调用系统调用的库函数:freadprintf

在这里插入图片描述

1.5 注意

系统调用是需要时间的,程序中频繁的使用系统调用会降低程序的运行效率。当运行内核代码时,CPU 工作在内核态,在系统调用发生前需要保存用户态的栈和内存环境,然后转入内核态工作。系统调用结束后,又要切换回用户态。这种环境的切换会消耗掉许多时间。

2. 文件操作

2.1 文件描述符概念

文件描述符是一个非负整数,代表已打开的文件。

每一个进程都会创建一张文件描述符表 记录的是当前进程打开的所有文件描述符。

每一个进程默认打开三个文件描述符:

  • 0(标准输入设备scanf)
  • 1(标准输出设备printf)
  • 2(标准错误输入设备perror)。

新打开的文件描述符 为 最小可用文件描述符,一般以3开始。

在这里插入图片描述

位图表示:

在这里插入图片描述

2.1.1 ulimit 临时修改资源限制

ulimit是一个计算机命令,用于shell启动进程所占用的资源,可用于修改系统资源限制。使用ulimit命令用于 临时 修改资源限制,如果需要永久修改需要将设置写入配置文件/etc/security/limits.conf。

ulimit -a 查看open files打开的文件最大数。
ulimit -n 最大数 设置open files打开的文件最大数。  

在这里插入图片描述
在这里插入图片描述

2.2 文件读写

2.2.1 文件磁盘权限

在这里插入图片描述

解释:
    第一位说是文件还是文件夹
    2~4位说明所有者权限
    5~7位说明同组用户权限
    8~10位说明其他用户权限

r	4
w	2
x	1

注意:

man 2 系统调用
在终端下 查看系统调用函数对应的头文件与函数信息  
2.2.2 open 打开文件
//所需头文件
    #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:代码操作文件的权限
    • 必选项
      • O_RDONLY 以只读的方式打开
      • O_WRONLY 以只写的方式打开
      • O_RDWR 以可读、可写的方式打开
    • **可选项 **
      • O_CREAT 文件不存在则创建文件,使用此选项时需使用 mode 说明文件的权限
      • O_EXCL 如果同时指定了 O_CREAT,且文件已经存在,则打开,如果文件不存在则新建
      • O_TRUNC 如果文件存在,则清空文件内容
      • O_APPEND 写文件时,数据添加到文件末尾
      • O_NONBLOCK 对于设备文件,以 O_NONBLOCK 方式打开可以做 非阻塞
  • mode:文件在磁盘中的权限
    • 格式:
      • 0ddd
      • d的取值:4(可读),2(可写),1(可执行)
      • 第一个d:所有者权限
      • 第二个d:同组用户权限
      • 第三个d:其他用户权限
      • 如果需要可读可写就是6,可读可执行5等
    • 如:
      0666:所有者可读可写,同组用户可读可写,其他用户可读可写
      0765:所有者可读可写可执行,同组用户可读可写,其他用户可读可执行

返回值:

成功:得到最小可用的文件描述符

失败:-1

建议:

操作已有文件使用两参

新建文件使用三参

2.2.3 close 关闭文件
//所需头文件
	#include <unistd.h>

//函数
	int close(int fd);

参数:

  • 关闭的文件描述符
  • 返回值
    • 成功:0
    • 失败: -1,并设置 errno

示例:以写的方式打开关闭文件

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

int main(int argc, char const *argv[])
{
    //文件不存在则创建文件
    int fd = open("test.txt", O_WRONLY|O_CREAT|O_APPEND, 0765);
    printf("fd=%d\n", fd);
    if(fd < 0)
    {
        printf("文件打开失败\n");
        return 0;
    }
    int x = close(fd);
    printf("x=%d\n", x);
    if(x < 0)
    {
        printf("文件关闭失败\n");
        return 0;
    }
    return 0;
}

// fd=3
// x=0
2.2.4 write写入
//所需头文件
	#include <unistd.h>
//函数
	ssize_t write(int fd, const void *buf, size_t count);

参数:

  • fd:写入的文件描述符
  • buf:写入的内容首地址
  • count:写入的长度,单位字节

返回值

  • 成功:返回 写入的内容的长度,单位字节
  • 失败:-1

示例:

#include <stdio.h>
#include <sys/types.h>      //open
#include <sys/stat.h>       //open
#include <fcntl.h>          //open
#include <unistd.h>         //close
int main(int argc, char const *argv[])
{
    int fd = open("test.txt", O_WRONLY | O_APPEND);
    if(fd < 0)
    {
        printf("文件打开失败\n");
        return 0;
    }
    char buf[] = "hello";
    //-1是为了去除字符串自带的\0
    int len = write(fd, buf, sizeof(buf) - 1);
    if(len < 0)
    {
        printf("写入失败\n");
    }
    if(close(fd) < 0)
    {
        printf("关闭失败\n");
    }
}
2.2.5 read读取
//所需头
	#include <unistd.h>

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

参数:

  • fd:文件描述符
  • buf:内存首地址
  • count:读取的字节个数

返回值:

  • 成功:实际 读取到的字节个数
  • 失败:-1

示例:

#include <stdio.h>
#include <sys/types.h>      //open
#include <sys/stat.h>       //open
#include <fcntl.h>          //open
#include <unistd.h>         //close
int main(int argc, char const *argv[])
{
    int fd = open("test.txt",O_RDONLY);
    char buf[100] = "";
    int len = read(fd,buf,100);
    printf("len = %d\n",len);
    //buf的第len位赋值'\0',buf在此处结束
    buf[len] = '\0';
    printf("buf = %s\n",buf);
    close(fd);
    return 0;
}

// len = 5
// buf = hello

2.3 练习:文件的复制

#include <stdio.h>
#include <sys/types.h>      //open
#include <sys/stat.h>       //open
#include <fcntl.h>          //open
#include <unistd.h>         //close
int main(int argc, char const *argv[])
{
    //1、打开文件2个文件,一个读的文件,一个写入的文件
    int fd_r = open("test.txt", O_RDONLY);
    if(fd_r < 0)
    {
        printf("读文件打开失败");
        return 0;
    }
    int fd_w = open("info.txt", O_WRONLY | O_CREAT | O_APPEND, 0765);
    if(fd_w < 0)
    {
        printf("写文件打开失败");
        return 0;
    }
    //2、读取读文件中的内容(循环读取)
    while(1)
    {
        //大文件,一般一次读取的大小为1024
        char buf[10] = "";
        //len : 实际读取到的字节数
        int len = read(fd_r, buf, sizeof(buf));
        //3,将读取到的内容,写入到写文件中
        write(fd_w, buf, len);
        if(len < sizeof(buf))
        {
            break;
        }
    }
    return 0;
}

2.4 文件的阻塞特性

read默认为阻塞。如果读不到数据,将阻塞不继续执行 知道有数据可读,才继续往下执行。
非阻塞特性:如果没数据,立即返回,继续执行。
注意:阻塞与非阻塞是对于文件而言的,而不是指 read、write 等的属性。

示例:

#include <stdio.h>
#include <sys/types.h>      //open
#include <sys/stat.h>       //open
#include <fcntl.h>          //open
#include <unistd.h>         //close
int main(int argc, char const *argv[])
{
    // /dev/tty,当前终端对应的设备文件
    //O_NONBLOCK:设置打开的文件为非阻塞
    int fd = open("/dev/tty",O_RDONLY | O_NONBLOCK);
    char buf[10] = "";
    printf("请输入:\n");
    int len = read(fd,buf,10);
    close(fd);
    printf("buf = %s\n",buf);
    return 0;
}

结果:

在这里插入图片描述

通过open打开的文件可以设置非阻塞,但是如果不是通过open打开的文件怎么办?

通过fcntl函数来解决

2.4.1 fcntl 设置阻塞状态(已存在文件)

作用:针对已经存在的文件描述符设置阻塞状态

//所需头文件
    #include <unistd.h>
    #include <fcntl.h>

//函数:
	int fcntl(int fd, int cmd, ... /* arg */);

功能:

  • 改变已打开的文件性质,fcntl 针对描述符提供控制。

参数:

  • fd:操作的文件描述符
  • cmd:操作方式
  • arg:针对 cmd 的值,fcntl 能够接受第三个参数 int arg。

返回值:

  • 成功:返回某个其他值
  • 失败:-1

fcntl 函数有 5 种功能:

    1. 复制一个现有的描述符(cmd=F_DUPFD)
    1. 获得/设置文件描述符标记(cmd=F_GETFD 或 F_SETFD)
    1. 获得/设置文件状态标记(cmd=F_GETFL 或 F_SETFL)
    1. 获得/设置异步 I/O 所有权(cmd=F_GETOWN 或 F_SETOWN)
    1. 获得/设置记录锁(cmd=FGETLK, F_SETLK 或 F_SETLKW)

使用步骤:

  • 获取文件状态标记
  • 将得到的文件状态标记设置为非阻塞
  • 将修改后的文件非阻塞状态标记,设置到当前文件描述符中

示例:

#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
int main(int argc, char const *argv[])
{ 
    //1,获取文件标记状态
    int status = fcntl(0,F_GETFL);
    //2修改状态为非阻塞状态
    status = status | O_NONBLOCK;
    //3,设置文件标记状态为非阻塞状态
    fcntl(0,F_SETFL,status);
    char buf[32]="";
    printf("开始读取\n");
    //0(标准输入设备scanf)
    int len = read(0,buf,sizeof(buf));
    printf("结束读取,读取到的内容为:%s\n",buf);
    return 0;
}

2.5 文件状态

作用:获取文件状态信息

//所需头
    #include <sys/types.h>
    #include <sys/stat.h>
    #include <unistd.h>

//函数
    int stat(const char *path, struct stat *buf);
    int lstat(const char *pathname, struct stat *buf);

参数:

  • 1参:文件地址
  • 2参:保存文件信息的结构体

返回值

  • 0:成功
  • -1:失败
2.5.1 stat与lstat的区别

当文件是一个符号链接时:

  • lstat 返回的是该符号链接本身的信息;
  • stat 返回的是该链接指向的文件的信息。
2.5.2 stat结构体解释
struct stat {
    dev_t st_dev; //文件的设备编号
    ino_t st_ino; //节点
    mode_t st_mode; //文件的类型和存取的权限
    nlink_t st_nlink; //连到该文件的硬连接数目,刚建立的文件值为 1
    uid_t st_uid; //用户 ID
    gid_t st_gid; //组 ID
    dev_t st_rdev; //(设备类型)若此文件为设备文件,则为其设备编号
    off_t st_size; //文件字节数(文件大小)
    blksize_t st_blksize; //块大小(文件系统的 I/O 缓冲区大小)
    blkcnt_t st_blocks; //块数
    time_t st_atime; //最后一次访问时间
    time_t st_mtime; //最后一次修改时间
    time_t st_ctime; //最后一次改变时间(指属性)
};

stat结构体st_mode属性:

一个由16个字节组成,简称16位
0~2其他人权限
3~5所属组权限
6~8所有者权限
12~15文件类型
具体参考下图

在这里插入图片描述

存储权限

S_ISUID 04000 set-user-ID bit
S_ISGID 02000 set-group-ID bit (see below)
S_ISVTX 01000 sticky bit (see below)

S_IRWXU 00700 owner has read, write, and execute permission
S_IRUSR 00400 owner has read permission
S_IWUSR 00200 owner has write permission
S_IXUSR 00100 owner has execute permission

S_IRWXG 00070 group has read, write, and execute permission
S_IRGRP 00040 group has read permission
S_IWGRP 00020 group has write permission
S_IXGRP 00010 group has execute permission

S_IRWXO 00007 others (not in group) have read, write, and execute permission
S_IROTH 00004 others have read permission
S_IWOTH 00002 others have write permission
S_IXOTH 00001 others have execute permission

示例:

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
int main(int argc, char const *argv[])
{
    struct stat buf;
    int tag = stat("test.txt",&buf);
    if (tag < 0)
    {
        printf("读取失败\n");
        return 0;
    }

    //说明当前文件为普通文件
    if (S_ISREG(buf.st_mode))
    {
        printf("test.txt是文件\n");
    }
    else if(S_ISDIR(buf.st_mode))
    {
        printf("test.txt是目录\n");
    }   
    
    printf("当前文件的大小为:%d\n",buf.st_size);
    return 0;
}

// test.txt是文件
// 当前文件的大小为:5

3. 目录操作

3.1 打开目录 opendir

作用:打开目录

//所有头文件:
    #include <sys/types.h>
    #include <dirent.h>

//函数:
	DIR *opendir(const char *name);

参数:

  • name:目录名

返回值:

  • 成功:返回指向该目录结构体指针(DIR *)
  • 失败:NULL
  • DIR:中文名称句柄,其实就是目录的结构体指针

3.2 读取目录 readdir

作用:读取目录

//所需头文件
	#include <dirent.h>

//函数
	struct dirent *readdir(DIR *dirp);

参数:

  • dirp:opendir 的返回值

返回值:

  • 成功:目录结构体指针
  • 失败:NULL

注意:一次读取一个文件。

相关结构体:

struct dirent
{
    ino_t d_ino; // 此目录进入点的 inode
    off_t d_off; // 目录文件开头至此目录进入点的位移
    signed short int d_reclen; // d_name 的长度, 不包含 NULL 字符
    unsigned char d_type; // d_type 所指的文件类型
    char d_name[256]; // 文件名
};

d_type说明:

  • DT_BLK这是一个块设备。(块设备如:磁盘)
  • DT_CHR这是一个字符设备。(字符设备如:键盘,打印机)
  • DT_DIR这是一个目录。
  • DT_FIFO这是一个命名管道(FIFO)。
  • DT_LNK这是一个符号链接。
  • DT_REG这是一个常规文件。
  • DT_SOCK这是一个UNIX域套接字。
  • DT_UNKNOWN文件类型未知。

3.3 关闭目录 closedir

作用:关闭目录

//所需头文件
    #include <sys/types.h>
    #include <dirent.h>

//函数
	int closedir(DIR *dirp);

参数:

  • dirp:opendir 返回的指针

返回值:

  • 成功:0
  • 失败:-1

3.4 示例1

打印制定目录下的文件名

#include <stdio.h>
#include <sys/types.h>
#include <dirent.h>
#include <string.h>

int main(int argc, char const *argv[])
{
    DIR *dir = opendir("/home/zhb/iot2302_3/02_file");
    if(dir == NULL)
    {
        printf("打开目录失败");
        return 0;
    }
    while(1)
    {
        struct dirent *p;
        p = readdir(dir);
        if(p == NULL)
        {
            break;
        }
        //.和.. 排除
        if(strcmp(p->d_name, ".") == 0 || strcmp(p->d_name, "..") == 0)
        {
            continue;;
        }
        printf("%s\n", p->d_name);
    }
    return 0;
}

// info.txt
// 07_code.c
// 01_code.c
// 05_code.c
// 06_code.c
// 00_code.c
// 03_code.c
// test.txt
// a.out

3.5 示例2 递归打印文件夹、文件名

#include <stdio.h>
#include <sys/types.h>
#include <dirent.h>
#include <string.h>

void traversalDir(char *path)
{
    char filepath[256] = "";
    strcpy(filepath, path);
    DIR *dir = opendir(filepath);
    if(dir == NULL)
    {
        printf("打开目录失败\n");
        return;
    }
    while (1)
    {
        struct dirent *p;
        p = readdir(dir);
        if(p == NULL)
        {
            break;
        }
        if(p->d_type == DT_DIR && strcmp(p->d_name, ".") != 0 && strcmp(p->d_name, "..") != 0)
        {
            //拼接目录下的目录递归遍历
            char newpath[256] = "";
            strcpy(newpath, filepath);
            strcat(newpath, "/");
            strcat(newpath, p->d_name);
            printf("%s\n", newpath);
            traversalDir(newpath);
        }
        if (p->d_type == DT_REG)
        {
            printf("%s/%s\n", filepath, p->d_name);
        }
    }
    closedir(dir);
}

int main(int argc, char const *argv[])
{
    traversalDir("/home/zhb/iot2302_3");
    return 0;
}
文章来源:https://blog.csdn.net/zhb_feng/article/details/135471470
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。