Ⅰ. open/close/read/write 打开/关闭/读/写

  • open
  • 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);
    1. 返回值:成功返回新分配的文件描述符,出错返回-1并设置errno
    2. pathname参数是要打开或创建的文件名,既可以是绝对路径也可以是相对路径
    3. flags参数有一系列常数值可供选择,可以同时选择多个常数用按位或运算符连接起来,所以这些常数的宏定义都以O_开头,表示or
      1. 必选项:以下三个常数中必须指定一个,且仅允许指定一个。
        • O_RDONLY:只读打开
        • O_WRONLY:只写打开
        • O_RDWR:可读可写打开
      2. 可选项:可以同时指定0个或多个,和必选项按位或起来作为flags参数。
        • O_CREAT:若此文件不存在则创建它。必须要提供第三个参数mode,表示文件的访问权限。
        • O_EXCL:如果同时指定了O_CREAT,并且文件已存在,则返回错误。
        • O_TRUNC:如果文件已存在,并且以只读或可读可写方式打开,则将其长度截断为0字节。
        • O_APPEND:表示追加。如果文件已有内容,这次打开文件所写的数据附加到文件的末尾。
        • O_NONBLOCK:对于设备文件,以O_NONBLOCK方式打开可以做非阻塞I/O。
    4. mode参数指定文件权限,可以用八进制数表示,比如0644表示-rw-r--r--,也可以用 S_IRUSR、S_IWUSR 等宏定义按位或起来表示。
    #include <fcntl.h>
    #include <stdio.h>
    #include <stdlib.h>
    #include <sys/stat.h>
    #include <sys/types.h>
    
    int main(int argc, char* argv[]) {
        if (argc < 2) {
            printf("usage:cmd filename\n");
            return -1;
        }
    
        // int fd = open(argv[1], O_WRONLY | O_CREAT, 0644); //w+,写文件,如果文件不存在则创建。成功返回新分配的文件描述符,出错返回-1并设置errno
        // int fd = = open(argv[1], O_WRONLY | O_CREAT | O_EXCL, 0644); // 写文件,如果文件不存在则创建,如果文件存在则返回-1并设置errno
        int fd = open(argv[1], O_WRONLY | O_CREAT | O_TRUNC, 0644); // 写文件,如果文件存在则截断为0字节,如果文件不存在则创建它
        if (fd < 0) {
            perror("OPEN");  // perror作用:翻译错误码errno为对应的描述
            exit(-1);
        }
    
        return 0;
    }

  • close
  • close函数关闭一个已打开的文件

    #include <unistd.h>
    int close(int fd);
    1. 返回值:成功返回 0, 出错返回-1并设置 erno
    2. 参数 fd 是要关闭的文件描述符。当一个进程终止时,内核对该进程所有尚未关闭的文件描述符调用 close 关闭,所以即使用户程序不调用 close,在终止时内核也会自动关闭它打开的所有文件。
      由 open 返回的文件描述符一定是该进程尚未使用的最小描述符。
    #include <fcntl.h>
    #include <stdio.h>
    #include <stdlib.h>
    #include <sys/stat.h>
    #include <sys/types.h>
    #include <unistd.h>
    
    int main(int argc, char* argv[]) {
        if (argc < 4) {
            printf("usage:cmd filename1 filename2 filename3\n");
            return -1;
        }
        // 测试open返回的文件描述符一定是该进程尚未使用的最小描述符
        // 思路:打开文件1,打开文件2,关闭文件1,打开文件3,关闭文件2,关闭文件3.输出文件描述符fd
        int fd1 = open(argv[1], O_WRONLY | O_CREAT, 0644);
        if (fd1 < 0) {
            perror("OPEN1");
            exit(-1);
        } else {
            printf("fd1=%d\n", fd1);
        }
        int fd2 = open(argv[2], O_WRONLY | O_CREAT, 0644);
        if (fd2 < 0) {
            perror("OPEN2");
            exit(-2);
        } else {
            printf("fd2=%d\n", fd2);
        }
        close(fd1);
        int fd3 = open(argv[3], O_WRONLY | O_CREAT, 0644);
        if (fd3 < 0) {
            perror("OPEN3");
            exit(-3);
        } else {
            printf("fd3=%d\n", fd3);
        }
        close(fd2);
        close(fd3);
    
        return 0;
    
        // 输出结果:
        // fd1=3
        // fd2=4
        // fd3=3
    }

  • read
  • read函数从已打开的设备或文件中读取数据

    #include <unistd.h>
    ssize_t read(int fd, void *buf, size_t count);
    1. 返回值:成功返回读取的字节数,出错返回-1 并设置 errno,如果在调 read 之前已到达文件末尾,则这次 read 返回 0
    2. 参数 count 是请求读取的字节数,读上来的数据保存在缓冲区 buf 中,同时文件的当前读写位置向后移。注意这个读写位置和使用C标准/0库时的读写位置有可能不同,这个读写位置是记在内核中的,而使用C标准I/0库时的读写位置是用户空间I/0缓冲区中的位置。
      有些情况下,实际读到的字节数(返回值)会小于请求读的字节数 count,例如:
      • 读常规文件时,在读到 count 个字节之前已到达文件末尾。
      • 从终端设备读,通常以行为单位,读到换行符就返回了。
    #include <fcntl.h>
    #include <stdio.h>
    #include <stdlib.h>
    #include <sys/stat.h>
    #include <sys/types.h>
    #include <unistd.h>
    
    int main(int argc, char* argv[]) {
        if (argc < 2) {
            printf("usage:cmd filename\n");
            return -1;
        }
        // 准备一个内容小于20个字符的文件,用来测试读到count个字节之前已到达文件末尾时会返回的情况
        int fd = open(argv[1], O_RDONLY);
        if (fd < 0) {
            perror("OPEN");
            exit(-1);
        } else {
            printf("fd=%d\n", fd);
        }
    
        char buf[20];
        ssize_t n;
        n = read(fd, buf, 10);
        printf("read %ld bytes\n", n);
        for (int i = 0; i < n; i++) {
            printf("%c", buf[i]);
        }
        printf("\n");
        n = read(fd, buf, 10);
        printf("read %ld bytes\n", n);
        for (int i = 0; i < n; i++) {
            printf("%c", buf[i]);
        }
        printf("\n");
    
        close(fd);
    
        return 0;
    }
    #include <fcntl.h>
    #include <stdio.h>
    #include <stdlib.h>
    #include <sys/stat.h>
    #include <sys/types.h>
    #include <unistd.h>
    
    int main(int argc, char* argv[]) {
        char buf[20];
        ssize_t n;
        n = read(STDIN_FILENO, buf, 10);  // 从标准输入中读取,以行为单位,读到换行符就会返回
        if (n < 0) {
            perror("READ STDIN");
            exit(-1);
        }
        printf("read %ld bytes\n", n);
        for (int i = 0; i < n; i++) {
            printf("%c", buf[i]);
        }
        putchar(10);  // 相当于printf("\n")
    
        return 0;
    }

  • write
  • write函数向已打开的设备或文件中写数据

    #include <unistd.h>
    ssite_t write(int fd, const void *buf, size_t count);
    1. 返回值:成功返回写入的字节数,出错返回-1并设置errno
    #include <fcntl.h>
    #include <stdio.h>
    #include <stdlib.h>
    #include <sys/stat.h>
    #include <sys/types.h>
    #include <unistd.h>
    
    int main(int argc, char* argv[]) {
        char buf[20];
        ssize_t n;
        n = read(STDIN_FILENO, buf, 10);  // 从标准输入中读取
        if (n < 0) {
            perror("READ STDIN");
            exit(-1);
        }
        printf("read %ld bytes\n", n);
        write(STDOUT_FILENO, buf, n);
        write(STDOUT_FILENO, "\n", 1);
    
        return 0;
    }

    Ⅱ. 阻塞与非阻塞

    当进程调用一个阻塞的系统函数时,该进程被置于睡眠(Sleep)状态,这时内核调度其它进程运行,直到该进程等待的事件发生了(比如网络上接收到数据包,或者调用 sleep 指定的睡眠时间到了)它才有可能继续运行。

    与睡眠状态相对的是运行(Running)状态,在 Linux 内核中,处于运行状态的进程分为两种情况:

    • 正在被调度执行:GPU 处于该进程的上下文环境中,程序计数器(eip)里保存着该进程的指令地址,正在执行该进程的指令,正在读写该进程的地址空间。
    • 就绪状态:该进程不需要等待什么事件发生,随时都可以执行,但 CPU 暂时还在执行另个进程,所以该进程在一个就绪队列中等待被内核调度。

    如果在 openー个设备时指定了 O_NONBLOCK 标志,read/ write 就不会阻塞。以 read 为例,如果设备暂时没有数据可读就返回-1, 同时置 errno为 EWOULDBL0K(或者 EAGAIN,这两个宏定义的值相同),表示本来应该阻塞在这里(would block,虚拟语气),事实上并没有阻塞而是直接返回错误,调用者应该试着再读一次(again)。这种行为方式称为轮询(Poll),调用者只是查询下,而不是阻塞在这里死等,这样可以同时监视多个设备:

    while(1){
        非阻塞read(设备1);
        if(设备1有数据到达)
            处理数据
        非阻塞read(设备2);
        if(设备2有数据到达)
            处理数据
        ...
        sleep(n);
    }
    #include <errno.h>
    #include <fcntl.h>
    #include <stdio.h>
    #include <stdlib.h>
    #include <sys/stat.h>
    #include <sys/types.h>
    #include <unistd.h>
    
    int main() {
        int fd = open("/dev/tty", O_RDONLY | O_NONBLOCK); // 从终端上非阻塞地读
        if (fd < 0) {
            perror("OPEN /dev/tty");
            exit(-1);
        }
        char buf[10];
        while (1) { // 轮询读
            ssize_t n = read(fd, buf, 10);
            if (n > -1) {
                // 读成功了
                printf("read %ld bytes\n", n);
                write(STDOUT_FILENO, buf, n);
                putchar(10);
                break;
            }
            if (errno != EAGAIN) {
                // 报错了
                perror("READ /dev/tty");
                exit(-2);
            }
            //errno == EAGAIN, 暂时没有数据可读
            write(STDOUT_FILENO, "try try?\n", 9);
            sleep(1);
        }
    
        close(fd);
    
        return 0;
    }