Linux系统编程(五)

写在前面

本文主要介绍Linux系统编程中的文件I/O

Linux系统调用

系统调用概述

顾名思义,说的是操作系统提供给用户程序调用的一组“特殊”接口。用户程序可以通过这组“特殊”接口来获得操作系统内核提供的服务,比如用户可以通过文件系统相关的调用请求系统打开文件、关闭文件或读写文件,可以通过时钟相关的系统调用获得系统时间或设置定时器等。
从逻辑上来说,系统调用可被看成是一个内核与用户空间程序交互的接口——它好比一个中间人,把用户进程的请求传达给内核,待内核把请求处理完毕后再将处理结果送回给用户空间。
系统服务之所以需要通过系统调用来提供给用户空间的根本原因是为了对系统进行“保护”,因为我们知道 Linux 的运行空间分为内核空间与用户空间,它们各自运行在不同的级别中,逻辑上相互隔离。所以用户进程在通常情况下不允许访问内核数据,也无法使用内核函数,它们只能在用户空间操作用户数据,调用用户空间函数。比如我们熟悉的“hello world”程序(执行时)就是标准的用户空间进程,它使用的打印函数 printf 就属于用户空间函数,打印的字符“hello word”字符串也属于用户空间数据。空间分布大概和前面GCC里面划的数据分段图差不多。

但是很多情况下,用户进程需要获得系统服务(调用系统程序),这时就必须利用系统提供给用户的“特殊接口”——系统调用了,它的特殊性主要在于规定了用户进程进入内核的具体位置;换句话说,用户访问内核的路径是事先规定好的,只能从规定位置进入内核,而不准许肆意跳入内核。有了这样的陷入内核的统一访问路径限制才能保证内核安全无误。我们可以形象地描述这种机制:作为一个游客,你可以买票要求进入野生动物园,但你必须老老实实地坐在观光车上,按照规定的路线观光游览。当然,不准下车,因为那样太危险,不是让你丢掉小命,就是让你吓坏了野生动物。

系统调用的实现

系统调用是属于操作系统内核的一部分的,必须以某种方式提供给进程让它们去调用。CPU 可以在不同的特权级别下运行,而相应的操作系统也有不同的运行级别,用户态和内核态。运行在内核态的进程可以毫无限制的访问各种资源,而在用户态下的用户进程的各种操作都有着限制,比如不能随意的访问内存、不能开闭中断以及切换运行的特权级别。显然,属于内核的系统调用一定是运行在内核态下,但是如何切换到内核态呢?

  • 答案是软件中断。
    软件中断和我们常说的中断(硬件中断)不同之处在于,它是通过软件指令触发而并非外设引发的中断,也就是说,又是编程人员开发出的一种异常(该异常为正常的异常)。操作系统一般是通过软件中断从用户态切换到内核态。
      中断有两个重要的属性,中断号和中断处理程序。中断号用来标识不同的中断,不同的中断具有不同的中断处理程序。在操作系统内核中维护着一个中断向量表(Interrupt Vector Table),这个数组存储了所有中断处理程序的地址,而中断号就是相应中断在中断向量表中的偏移量

系统调用和库函数的区别

  • 库函数由两类函数组成:

    • 不需要调用系统调用:不需要切换到内核空间即可完成函数全部功能,并且将结果反馈给应用程序,如strcpy、bzero 等字符串操作函数。
  • 需要调用系统调用

    • 需要切换到内核空间,这类函数通过封装系统调用去实现相应功能,如 printf、fread等。

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

库函数访问文件的时候根据需要,设置不同类型的缓冲区,从而减少了直接调用 IO 系统调用的次数,提高了访问效率,后面会对这两者进行对比。

文件I/O

文件描述符

文件描述符在PCB进程块内(本质上是一个结构体),PCB进程块内有一个成员为文件描述符表,表内的每一个指针指向一个文件描述符。
Linux 的世界里,一切设备皆文件。我们可以系统调用中 I/O 的函数(I:input,输入;O:output,输出),对文件进行相应的操作( open()、close()、write() 、read() 等)。

打开现存文件或新建文件时,系统(内核)会返回一个文件描述符,文件描述符用来指定已打开的文件。这个文件描述符相当于这个已打开文件的标号,文件描述符是非负整数,是文件的标识,操作这个文件描述符相当于操作这个描述符所指定的文件,类似于指针?

程序运行起来后(每个进程)都有一张文件描述符的表,标准输入、标准输出、标准错误输出设备文件被打开,对应的文件描述符 0、1、2 记录在表中。程序运行起来后这三个文件描述符是默认打开的。

#define STDIN_FILENO  0 //标准输入的文件描述符
#define STDOUT_FILENO 1 //标准输出的文件描述符
#define STDERR_FILENO 2 //标准错误的文件描述符

在程序运行起来后打开其他文件时,系统会返回文件描述符表中最小可用的文件描述符,并将此文件描述符记录在表中。Linux一个进程最多只能打开 NR_OPEN_DEFAULT (即1024)个文件,编号从0开始,最大文件描述符为1023,故当文件不再使用时应及时调用 close() 函数关闭文件。

阻塞/非阻塞

  • 阻塞:
    阻塞调用是指调用结果返回之前,当前线程会被挂起(线程进入非可执行状态,在这个状态下,cpu不会给线程分配时间片,即线程暂停运行)。
    函数只有在得到结果之后才会返回。
  • 非阻塞:
    非阻塞和阻塞的概念相对应,指在不能立刻得到结果之前,该函数不会阻塞当前线程,而会立刻返回。

阻塞/非阻塞是设备文件、网络文件的属性。产生阻塞的场景通常在,读取设备文件或者网络文件中,读取常规文件没有阻塞的概念,例如读取终端文件/dev/tty

常用I/O函数

open函数

函数原型:int open(const char *pathname, int flags, (mode_t mode));
作用:打开文件,如果文件不存在则创建。
需要的头文件:

  • #include <unistd.h>
  • #include <unistd.h>这个主要是用来控制flag参数的

参数:

  • pathname: 欲打开的文件路径名
  • flags:文件打开方式:O_RDONLY|O_WRONLY|O_RDWR
  • mode: 参数3使用的前提, 参2指定了 O_CREAT,这个参数,只有在文件不存在时有效,指新建文件时指定文件的权限。取值8进制数,用来描述文件的 访问权限。
    创建文件最终权限 = mode & ~umask

返回值:

  • 成功: 打开文件所得到对应的 文件描述符(整数)
  • 失败: -1, 设置errno

demo:

int fd = open("要打开的文件路径",O_RDWR|O_CREATE,0664);
if(fd == -1){
	perror("flie open error\n");
	exit(1);
}
close(fd);

close函数

函数原型:int close(int fd);
作用:关闭已打开的文件

参数:

  • fd: 文件描述符,open()的返回值

返回值:

  • 成功:0
  • 失败:-1

read函数

函数原型:ssize_t read(int fd, void *buf, size_t count);
作用:把指定数目的数据读到内存(缓冲区)

需要的头文件

  • #include <unistd.h>

参数

  • fd:文件描述符,open()的返回值
  • buf:存数据的缓冲区
  • count:缓冲区大小

返回值

  • 成功:返回读到的字节数
  • 失败:返回-1
    • 如果此时 errnoEAGINEWOULDBLOCK, 说明不是read失败,而是read在以非阻塞方式读一个设备文件(网络文件),并且文件无数据。

write函数

函数原型:ssize_t write(int fd, const void *buf, size_t count)
作用:把指定数目的数据写入内存(缓冲区)

需要的头文件

  • #include <unistd.h>

参数

  • fd:文件描述符,open()的返回值
  • buf:待写数据的缓冲区
  • count:要写入的数据大小

返回值

  • 成功:返回写入的字节数。
  • 失败: 返回-1并且设置 errno

实例

通过上述函数实现一个自定义的cp命令

#include<stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <stdlib.h>
int main(int argc, char *argv[]){
    int fd1, fd2;
    fd1 = open(argv[1], O_RDONLY); //由于是要复制,那么只需要用只读打开就行
    fd2 = open(argv[2], O_RDWR|O_CREAT|O_TRUNC, 0664);//背拷贝的文件可能不存在,如果不存在就创建,创建的文件权限为0664,如果文件存在则把文件截断,重新写入
    if(fd1 == -1){
        perror("file open error\n");
        exit(1);
    }   
    char buf[1024];
    int len;
    while( (len = read(fd1, buf, 1024)) != 0){ //循环读入到数据缓冲区
        if(len == -1){
            perror("read error\n");
            exit(1);
        }
        write(fd2, buf, len);
    }   
    close(fd1);
    close(fd2);
    return 0;
}

image.png

系统函数与库函数的区别
考虑到篇幅问题,点这里吧,这个问题非常重要

错误处理函数

返回只类型为voidlinux函数一般不会出错,但当一个函数出错时,errno(一个int型变量,用errno时,程序必须包含errno.h头文件)会随之改变,不同的值代表了不同的错误
所以直接用这个变量表示错误非常的不方便,所以每次想知道出现了什么错误,必须回到errno.h中察看宏定义
所以有以下几种方式来获得详细的错误信息:

  • void perror(const char *s);

函数的头文件是stdio.h
这个函数将上一个函数发生的错误输出到标准错误(stderr),参数s所指的参数先输出来(该参数与错误无> 关,可以自己任意添加),之后再输出错误原因(字符串),此原因依照errno的值来输出错误信息

  • char *strerror(int errnum);

函数说明:函数的头文件是string.h
参数一般为errno,可以把errno对应的数值转化成对应的错误信息(字符串)
printf("xxx error: %s\n", strerror(errno));

fcntl函数

函数原型:int fcntl(int fd, int cmd, ... /* arg */ );
作用:根据文件描述词来操作文件的特性。

需要的头文件

  • #include <unistd.h>
  • #include <fcntl.h>

参数

  • fd:文件描述符
  • cmd:描述fcntl函数要实现的功能

demo

//将文件在不重新打开的情况下由阻塞状态这是为非阻塞状态
int flgs = fcntl(fd,  F_GETFL);
flgs |= O_NONBLOCK
fcntl(fd,  F_SETFL, flgs);
//获取文件状态: F_GETFL
//设置文件状态: F_SETFL

fcntl设置文件属性.png

lseek函数

函数原型:off_t lseek(int fd, off_t offset, int whence);
作用:改变打开文件的读写偏移量

需要的头文件

  • #include <unistd.h>
  • #include <sys/types.h>

参数

  • fd:文件描述符
  • offset:根据参数whence来移动读写位置的位移数
  • whence:
    • SEEK_SET 参数offset 即为新的读写位置.
    • SEEK_CUR 以目前的读写位置往后增加offset 个位移量.
    • SEEK_END 将读写位置指向文件尾后再增加offset 个位移量.
    • 当whence 值为SEEK_CUR 或 SEEK_END 时, 参数offet 允许负值的出现.

返回值

  • 成功:较起始位置偏移量
  • 失败:返回-1并设置errno

应用场景:

  • 文件的“读”、“写”使用同一偏移位置。
  • 使用lseek获取文件大小
  • 使用lseek拓展文件大小:要想使文件大小真正拓展,必须引起IO操作。
    • 使用 truncate 函数,直接拓展文件。 int ret = truncate("dict.cp", 250);

文件系统、文件存储的一些概念

首先了解如下文件存储相关概念:inodedentry、数据存储、文件系统

  • inode:
    其本质为结构体,存储文件的属性信息,如权限、类型、大小、时间、用户、盘块位置……也叫做文件属性管理结构,大多数的inode都存储在磁盘上
    少量使用、近期使用的inode会被缓存到内存中
  • dentry
    目录项,其本质依然是结构体,重要成员变量有两个{文件名,inode},文件内的内容保存在磁盘块上,创建硬链接就是同一个inode号,当硬连接数为0即没有指向的时候磁盘就可以被覆盖。
    目录项和inode.png

stat/lstat 函数

函数原型:int stat(const char *path, struct stat *buf);
作用:获取文件的属性(从inode结构体中获取)

需要的头文件

  • #include <sys/types.h>
  • #include <sys/stat.h>
  • #include <unistd.h>

参数

  • path: 文件路径
  • buf:(传出参数) 存放文件属性

返回值

  • 成功:返回0
  • 失败:返回-1,设置errno
struct stat {
	dev_t     st_dev;         /* ID of device containing file */
	ino_t     st_ino;         /* inode number */
	mode_t    st_mode;        /* protection */
	nlink_t   st_nlink;       /* number of hard links */
	uid_t     st_uid;         /* user ID of owner */
	gid_t     st_gid;         /* group ID of owner */
	dev_t     st_rdev;        /* device ID (if special file) */
	off_t     st_size;        /* total size, in bytes */
	blksize_t st_blksize;     /* blocksize for filesystem I/O */
	blkcnt_t  st_blocks;      /* number of 512B blocks allocated */
	
	/* Since Linux 2.6, the kernel supports nanosecond
	  precision for the following timestamp fields.
	  For the details before Linux 2.6, see NOTES. */
	
	struct timespec st_atim;  /* time of last access */
	struct timespec st_mtim;  /* time of last modification */
	struct timespec st_ctim;  /* time of last status change */
	
	#define st_atime st_atim.tv_sec      /* Backward compatibility */
	#define st_mtime st_mtim.tv_sec
	#define st_ctime st_ctim.tv_sec
};

常用的:

  • buf.st_size:文件的大小
  • buf.st_mode:文件类型/文件的权限

st_mode使用说明

st_mode域是需要一些宏予以配合才能使用的。其实,通俗说,这些宏就是一些特定位置为1的二进制数的外
号,我们使用它们和st_mode进行”&”操作,从而就可以得到某些特定的信息。在第二卷的man手册中也有介绍,考虑篇幅问题,只列举常用的。
用于解释st_mode标志的掩码包括:

  • S_IFMT:文件类型
  • S_IRWXU:属主的读/写/执行权限,可以分成S_IXUSR, S_IRUSR, S_IWUSR
  • S_IRWXG:属组的读/写/执行权限,可以分成S_IXGRP, S_IRGRP, S_IWGRP
  • S_IRWXO:其他用户的读/写/执行权限,可以分为S_IXOTH, S_IROTH, S_IWOTH

还有一些用于帮助确定文件类型的宏定义,这些和上面的宏不一样,这些是带有参数的宏,类似与函数的使用方法:

  • S_ISBLK:测试是否是特殊的块设备文件
  • S_ISCHR:测试是否是特殊的字符设备文件
  • S_ISDIR:测试是否是目录(我估计find . -type d的源代码实现中就用到了这个宏)
  • S_ISFIFO:测试是否是FIFO设备
  • S_ISREG:测试是否是普通文件
  • S_ISLNK:测试是否是符号链接
  • S_ISSOCK:测试是否是socket

man手册中提供的两种判断文件属性的demo

//使用掩码
switch (sb.st_mode & S_IFMT) {
	case S_IFBLK:  printf("block device\n");            break;
	case S_IFCHR:  printf("character device\n");        break;
	case S_IFDIR:  printf("directory\n");               break;
	case S_IFIFO:  printf("FIFO/pipe\n");               break;
	case S_IFLNK:  printf("symlink\n");                 break;
	case S_IFREG:  printf("regular file\n");            break;
	case S_IFSOCK: printf("socket\n");                  break;
	default:       printf("unknown?\n");                break;
}
//使用宏函数
stat(pathname, &sb);
	if (S_ISREG(sb.st_mode)) {
		/* Handle regular file */
	} 
}

**lstatstat的区别在于,lstat不会符号穿透,stat会,当文件是一个符号链接时,lstat返回的是该符号链接本身的信息;而stat返回的是该链接指向的文件的信息。(似乎有些晕吧,这样记,lstat比stat多了一个l,因此它是有本事处理符号链接文件的,因此当遇到符号链接文件时,lstat当然不会放过。而 stat系统调用没有这个本事,它只能对符号链接文件睁一只眼闭一只眼,直接去处理链接所指文件喽)

参数

传入参数

  • 指针作为函数参数
  • 通常有const关键字修饰
  • 指针指向有效区域, 在函数内部做读操作
  • 传入参数为本身有值,传入函数让函数使用

传出参数

  • 指针作为函数参数
  • 在函数调用之前,指针指向的空间可以无意义,但必须有效
  • 在函数内部,做写操作
  • 函数调用结束后,充当函数返回值
  • 传出参数本身没值,从函数中带出值(相当于函数的返回值)

传出参数

  • 指针作为函数参数。
  • 在函数调用之前,指针指向的空间有实际意义。
  • 传出参数本身没值,从函数中带出值(相当于函数的返回值)
  • 函数调用结束后,充当函数返回值

link/unlink函数

盗一张图
清晰明了,i了i了。
Linux下删除文件的机制,不断的将st_nlink -1,直到为0时,无目录项对应的文件,将会被操作系统择机释放,具体由操作系统内部调度算法决定,因此,删除文件从某种意义上讲,是使文件有被释放的条件

  • 隐式回收:
    当进程结束运行时,所有该进程打开的文件会被关闭,申请的内存空间会被释放。系统的这一特性称之为隐式回收系统资源,但是写程序中不要依赖这个特性,因为这是操作系统的特性,但是我们跑程序的时候基本上是在服务器上,服务器一般是24h开着的,这就意味着操作系统没有这个机会来给你回收

目录操作函数

  • DIR * opendir(char *name);
  • int closedir(DIR *dp);
  • struct dirent *readdir(DIR * dp);
    和文件的操作函数差不多,不再过多赘述。

递归打印目录的内的文件,同时打印文件大小

#include <stdio.h>
#include <sys/stat.h>
#include <string.h>
#include <dirent.h>
#include <unistd.h>
#include <stdlib.h>
void getdir(const char *name);
void isDIR(const char *dirs);

void isDIR(const char *dirs){
    char name[270];
    struct dirent *ssd;
    DIR *dd = opendir(dirs);

    if(dd == NULL){
        perror("file dir error\n");
        return ;
    }   
    while( (ssd = readdir(dd)) != NULL){
        if( strcmp(ssd->d_name,".") == 0 || strcmp(ssd->d_name,"..") == 0) //防止死循环
            continue;
        sprintf(name, "%s/%s",dirs,ssd->d_name); //拼接字符串,假设此时ssd读到的内容是一个目录,那么想进入这个目录,必须有前面的路径加上这个d_name拼接出来的路径才能进入
        getdir(name);
    }   
    closedir(dd);   
}

void getdir(const char *name){
    struct stat buf;
    int flag = stat(name, &buf);
    if(flag == -1){
        perror("file name error\n");
        exit(1);
    }   
    
    if(S_ISDIR(buf.st_mode)){
        isDIR(name);
    }   

    printf("%ld %s\n",buf.st_size, name);
}

int main(int argc, char *argv[]){
    getdir(argv[1]);
    return 0;
}

查阅的资料