Linux文件,目录io类系统调用总结与示例

 
Category: Linux-Shell

写在前面

无论是做网络编程还是系统编程, 逃不开的一个内容就是C系统调用的学习, 正如C++的STL一样, 学习OS也有如下的三步骤:

  1. 会使用: 熟悉API
  2. 懂原理: 分析源码
  3. 写扩展: 实际开发

现在就来熟悉一下系统调用吧. 环境Ubuntu x86_64.

源码部分也参考了apue以及Linux/UNIX系统编程手册.

预备知识

什么是系统调用

  1. 系统调用将处理器从用户态切换到核心态, 以便让CPU访问受到保护的内核内存数据.
  2. 其组成是固定的, 每一个系统调用都由唯一一个数字来标识.

程序运行四区

截屏2023-02-04 00.37.08.jpg

非常重要, 全图背诵.

标准文件描述符

文件描述符 用途 POSIX名称 stdio流
0 标准输入 STDIN_FILENO stdin
1 标准输出 STDOUT_FILENO stdout
2 标准错误 STDERR_FILENO stderr

针对stdout调用freopen()函数后,无法保证stdout变量值仍然为1。

常用头文件

头文件 包括的常用函数/常量 作用
sys/types.h   类型定义
sys/stat.h   状态定义
stdio.h printf,fprintf 标准I/O函数
stdlib.h malloc,free 标准库函数
unistd.h sleep, 部分系统调用
errno.h   错误状态码
string.h memset,strcpy 字符创相关操作
(堆内存分配与初始化)
limits.h INT_MAX 系统限制
fcntl.h fcntl, 文件I/O函数(高级)

文件I/O

文件访问模式

常用的文件访问模式如下表.

访问模式 描述 访问模式 描述
O_RDONLY 只读打开 O_CREAT 不存在则创建
O_WRONLY 只写打开 O_TRUNC 截断已有文件
(长度置为0)
O_RDWR 读写打开 O_APPEND 文件尾部追加

备注:

  1. 调用open()时, O_RDONLY、O_WRONLY和O_RDWR标志在flags参数中不能同时使用,只能指定其中一种
  2. O_TRUNC: 如果文件已经存在且为普通文件,那么将清空文件内容,将其长度置0。在Linux下使用此标志,无论以读、写方式打开文件,都可清空文件内容(在这两种情况下,都必须拥有对文件的写权限)

权限位

权限位st_mode 含义 八进制值 英文注记
S_IRUSR 用户读 4 READ USER
S_IWUSR 用户写 2 WRITE USER
S_IXUSR 用户执行 1 EXEC USER
S_IRGRP 组读 4 READ GROUP
S_IWGRP 组写 2 WRITE GROUP
S_IXGRP 组执行 1 EXEC GROUP
S_IROTH 其他用户读 4 READ OTHER
S_IWOTH 其他用户写 2 WRITE OTHER
S_IXOTH 其他用户执行 1 EXEC OTHER

例如常用的可执行文件权限位: 755, 就对应了rwx-r-xr-x, 而默认创建目录的权限位为:

open: 创建文件

fd = open(pathname, flags, mode) 函数打开pathname所标识的文件,并返回文件描述符,用以在后续函数调用中指代打开的文件。如果文件不存在,open()函数可以创建之,这取决于对位掩码参数flags的设置。

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

void error_handling(char* msg);

int fd;
void t1() {
    // create and write
    char buf[] = "Let's go!";

    fd = open("data", O_RDWR | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR);
    if (fd == -1) error_handling("open() error");
    printf("file descripter: %d\n", fd);

    if (write(fd, buf, sizeof(buf)) == -1) error_handling("write() error");

    close(fd);
}
void t2() {
    // append to log
    char buf[] = "abc\n";
    fd = open("log", O_WRONLY | O_CREAT | O_APPEND, S_IRUSR | S_IWUSR);
    if (fd == -1) error_handling("open() error");
    printf("file descripter: %d\n", fd);

    if (write(fd, buf, sizeof(buf)) == -1) error_handling("write() error");
    close(fd);
}


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


void error_handling(char* msg) {
    fputs(msg, stderr);
    fputc('\n', stderr);
    /* printf("aa\n"); */
    exit(1);
}

read: 读出流

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

void t1() {
    int fd = open("data", O_RDONLY);
    char buf[100];
    read(fd, buf, 10);
    printf("buf=%s\n", buf); // buf=Let's go!

    close(fd);
}
int main(int argc, char *argv[]) {
    t1();
    return 0;
}

write: 写入流

例子见open().

close: 关闭文件

顾名思义.

lseek: 改变文件偏移量

对于每个打开的文件,系统内核会记录其文件偏移量,有时也将文件偏移量称为读写偏移量或指针。文件偏移量是指执行下一个read()或write()操作的文件起始位置,会以相对于文件头部起始点的文件当前位置来表示。文件第一个字节的偏移量为0。

文件打开时,会将文件偏移量设置为指向文件开始,以后每次read()或write()调用将自动对其进行调整,以指向已读或已写数据后的下一字节。因此,连续的read()和write()调用将按顺序递进,对文件进行操作。 针对文件描述符fd参数所指代的已打开文件,lseek()系统调用依照offset和whence参数值调整该文件的偏移量。

offset参数指定了一个以字节为单位的数值。(SUSv3规定off_t数据类型为有符号整型数。)whence参数则表明应参照哪个基点来解释offset参数,应为下列其中之一:

  • SEEK_SET: 将文件偏移量设置为从文件头部起始点开始的offset个字节
  • SEEK_CUR: 相对于当前文件偏移量,将文件偏移量调整offset个字节.
  • SEEK_END: 将文件偏移量设置为起始于文件尾部的offset个字节。也就是说,offset参数应该从文件最后一个字节之后的下一个字节算起.
#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

void t1() {
    int fd = open("data", O_WRONLY, S_IWUSR);
    int cur = lseek(fd, 0, SEEK_CUR);
    printf("cur seek = %d\n", cur);
    cur = lseek(fd, 2, SEEK_SET);
    printf("cur seek = %d\n", cur);
}

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

stat: 文件信息

#include <stdio.h>
#include <time.h>
#include <sys/stat.h>

int main(int argc, char *argv[]) {
    struct stat sb;
    if (stat("data", &sb) == -1) fprintf(stderr, "stat-error");
    printf("size=%ld\n", sb.st_size);
    printf("mode=%u\n", sb.st_mode);
    printf("uid=%u\n", sb.st_uid);
    printf("gid=%u\n", sb.st_gid);
    printf("hard link number=%ld\n", sb.st_nlink);
    putchar('\n');
    printf("dev-id=%ld\n",sb.st_dev);
    printf("rdev-id=%ld\n", sb.st_rdev);
    printf("i-node=%ld\n", sb.st_ino);
    printf("block-size=%ld\n", sb.st_blksize);
    printf("blocks=%ld\n", sb.st_blocks);
    putchar('\n');
    printf("last access time=%s", ctime(&sb.st_atime));
    printf("last modify time=%s", ctime(&sb.st_mtime));
    printf("last status change time=%s\n", ctime(&sb.st_ctime));
    return 0;
}
/* size=10 */
/* mode=33152 */
/* uid=1001 */
/* gid=1001 */
/* hard link number=1 */
/*  */
/* dev-id=64513 */
/* rdev-id=0 */
/* i-node=1212099 */
/* block-size=4096 */
/* blocks=8 */
/*  */
/* last access time=Thu Feb  9 17:42:47 2023 */
/* last modify time=Sun Feb  5 01:19:56 2023 */
/* last status change time=Sun Feb  5 01:19:56 2023 */ 

mkstemp, tmpfile: 创建临时文件

mkstemp

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

int main(int argc, char* argv[]) {
    char tmp[] = "/tmp/test-XXXXXX";
    // must use char xx[], not char*
    // must use XXXXXX not xxxxxx
    int fd = mkstemp(tmp);
    if (fd == -1) fprintf(stderr, "mkstemp");
    sleep(2);
    printf("temp file name is %s\n", tmp);
    printf("temp file fd is %d\n", fd);
    sleep(2);
    unlink(tmp);

    if (close(fd) == -1) fprintf(stderr, "close");
    /* temp file name is /tmp/test-Y4PE69 */
    /* temp file fd is 3 */
    return 0;
}

可以用watch来监视文件创建情况: (开两个终端)

watch -n 0.1 ls /tmp
test-ksJ1KT

close后消失.

tmpfile

tmpfile返回文件指针.

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

int main(int argc, char* argv[]) {
    FILE* f1 = tmpfile(); // delete filename when create it
    if (f1 == NULL) fprintf(stderr, "tmpfile");

    const char s[30] = {"hello tmpfile"};
    fputs(s, f1);
    fseek(f1, 0, 0); // 回到文件开头

    char ans[20];
    fgets(ans, 20, f1);
    printf("%s\n", ans); // hello tmpfile

    fclose(f1);
    return 0;
}

fcntl: 获取文件打开模式与状态标志

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>

int main(int argc, char *argv[]) {
    char tmp[] = "/tmp/test-XXXXXX";
    int fd = mkstemp(tmp);
    int flags, accseeMode;

    flags = fcntl(fd, F_GETFL);
    if (flags == -1) fprintf(stderr, "fcntl\n");

    // 判断访问模式: 是否同步模式
    if (flags & O_SYNC) printf("writes are synchronized\n");

    // 判断状态标志
    accseeMode = flags & O_ACCMODE;
    if (accseeMode == O_WRONLY || accseeMode == O_RDWR)
        printf("file is writable\n");

    /* file is writable */

    unlink(tmp);
    close(fd);
    return 0;
}

通过传入的fd可以改变相应文件的状态标志:

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>

int main(int argc, char *argv[]) {
    char tmp[] = "/tmp/test-XXXXXX";
    int fd = mkstemp(tmp);
    int flags;

    flags = fcntl(fd, F_SETFL);
    if (flags == -1) fprintf(stderr, "fcntl\n");

    flags |= O_APPEND;
    // 更新状态标志
    if (fcntl(fd, F_SETFL, flags) == -1) fprintf(stderr, "fcntl F_SETFL");

    if (flags & O_APPEND) printf("file can be appended\n");

    /* file can be appended */

    unlink(tmp);
    close(fd);
    return 0;
}

目录I/O

mkdir: 创建目录

#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <fcntl.h>

// rw-r--r--
#define FILE_MODE (S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)
// rwxrwxr-x
#define DIR_MODE (FILE_MODE | S_IXUSR | S_IWGRP | S_IXGRP | S_IXOTH)

int main(int argc, char *argv[]) {
    mkdir("new_dir", DIR_MODE);
    return 0;
}

getpwd: 获取进程的当前工作目录

#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>


int main(int argc, char *argv[]) {
    char *ptr;
    size_t size = 100;
    ptr = (char *)malloc(size);

    getcwd(ptr, size);
    printf("cwd = %s\n", ptr); // cwd = /home/zorch/code/c_cpp_code/syscall/dirio
    return 0;
}

chdir: 更改当前目录

#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>


int main(int argc, char *argv[]) {
    char *ptr;
    size_t size = 100;
    ptr = (char *)malloc(size);
    chdir("/usr/local/lib");

    getcwd(ptr, size);
    printf("cwd = %s\n", ptr); // cwd = /usr/local/lib

    return 0;
}

realpath: 解引用所有的符号链接.

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/stat.h>
#include <limits.h>
const int BUF_SIZE = 1000;
const char* argv1 = "aa";

int main(int argc, char* argv[]) {
    struct stat statbuf;
    char buf[BUF_SIZE];

    if (lstat(argv1, &statbuf) == -1) fprintf(stderr, "lstat error\n");

    if (!S_ISLNK(statbuf.st_mode))
        fprintf(stderr, "%s is not a symbolic link\n", argv1);

    ssize_t numBytes = readlink(argv1, buf, BUF_SIZE - 1);
    if (numBytes == -1) fprintf(stderr, "readlink\n");
    buf[numBytes] = '\0';

    printf("readlink : %s -> %s\n", argv1, buf);
    if (realpath(argv1, buf) == NULL) fprintf(stderr, "realpath error\n");

    printf("realpath=%s -> %s\n", argv1, buf);
    return 0;
}
// ln -s chdir-1.c aa
/* readlink : aa -> chdir-1.c */
/* realpath=aa -> /home/zorch/code/c_cpp_code/syscall/dirio/chdir-1.c */

dirname, basename: 解析路径名字符串

可能修改源字符串内容, 所以复制一份新的字符串, strdup函数内部用到了malloc分配堆内存, 所以之后需要free掉.

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

int main(int argc, char* argv[]) {
    char* path = "/home/zorch/Desktop/firefox.desktop";
    char *t1, *t2;
    t1 = strdup(path);
    t2 = strdup(path);
    printf("dirname is %s\n", dirname(t1));
    printf("basename is %s\n", basename(t2));
    free(t1);
    free(t2);
    return 0;
}
/* dirname is /home/zorch/Desktop */
/* basename is firefox.desktop */