文章目录

一、汇编程序的Hello world

1.基础知识

  • 各种Linux系统函数,这些函数的用法必须结合Linux内核的工作原理来理解,因为系统函数正是内核提供给应用程序的接口
  • 因为每种UNIX系统的内核都不一样。这里只讲Linux平台的特性,只讲Linux内核的工作原理,涉及体系结构时只讲x86平台
  • 也会尽量兼顾可移植性,避免依赖于Linux平台特有的一些特性

2.I/O操作是怎样通过系统调用传给内核?

  • eg:汇编程序的Hello world
    (第28章)Linux系统编程之文件与IO_库函数
  • 解释一下这行代码中的“.”, 汇编器总是从前到后把汇编代码转换成目标文件,在这个过程中维护一个地址计数器,当处理到每个段的开头时把地址计数器置成0,然后每处理一条汇编指示或指令就把地址计数器增加相应的字节数,在汇编程序中用“.”可以取出当前地址计数器的值,该值是一个常量。
  • 在汇编程序的_start的系统调用中,在调 write 系统调用时, eax 寄存器保存着 write 的系统调用号4, ebx 、 ecx 、 edx 寄存器分别保存着 write 系统调用需要的三个参数
  • 在C代码的系统调用中,
    (第28章)Linux系统编程之文件与IO_#include_02
二、C标准I/O库函数与Unbuffered I/O函数

1.C标准I/O库函数实现系统调用的过程

(第28章)Linux系统编程之文件与IO_其他_03
(第28章)Linux系统编程之文件与IO_其他_04

2.C库函数与系统调用的层次关系

(第28章)Linux系统编程之文件与IO_库函数_05

3.C标准I/O库函数与直接调用底层的unbuffered I/O函数的区别?

  • C标准I/O库函数有I/O缓冲区buffer,而open 、 read 、 write 、 close 等系统函数称为无缓冲I/O(Unbuffered I/O) 函数,因为它们位于C标准库的I/O缓冲区的底层

(第28章)Linux系统编程之文件与IO_文件描述符_06
(第28章)Linux系统编程之文件与IO_#include_07

  • 只有在UNIX平台上才能使用Unbuffered I/O函数,所以C标准I/O库函数在头文件 stdio.h 中声明,而 read 、 write 等函数在头文件 unistd.h 中声明,所以,在支持C语言的非
    UNIX操作系统上,标准I/O库的底层可能由另外一组系统函数支持,例如Windows系统的底层是Win32 API,其中读写文件的系统函数是 ReadFile 、 WriteFile

4.Unix标准

(1)POSIX标准的前世今生

(第28章)Linux系统编程之文件与IO_#include_08

(2)进程描述符,进程控制块PCB,文件描述符表

(第28章)Linux系统编程之文件与IO_库函数_09
(第28章)Linux系统编程之文件与IO_其他_10

(3)C标准库和底层I/O函数在标准输入、标准输出、和标准错误输出的区别

(第28章)Linux系统编程之文件与IO_#include_11

三、open/close函数

1.open 函数可以打开或创建一个文件

(1)源码如下:

#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并设置errno

(1)在Man Page中 open 函数有两种形式,一种带两个参数,一种带三个参数,最后的可变参数可以是0个或1个,由 flags 参数中的标志位决定;

(2)pathname参数
pathname 参数是要打开或创建的文件名,和 fopen 一样, pathname 既可以是相对路径也可以是绝对路径。

(3)flags参数
flags 参数有一系列常数值可供选择,可以同时选择多个常数用按位或运算符连接起来,所以这些常数的宏定义都以 O_ 开头,表示or。

  • 必选项:以下三个常数中必须指定一个,且仅允许指定一个
    (第28章)Linux系统编程之文件与IO_库函数_12
  • 以下可选项可以同时指定0个或多个,和必选项按位或起来作为 flags 参数,可选项有很多,这里只介绍一部分,其它选项可参考 open(2) 的Man Page:
    (第28章)Linux系统编程之文件与IO_其他_13
    (4)mode参数
    (第28章)Linux系统编程之文件与IO_#include_14

2.open函数与C标准I/O库的 fopen 函数有些细微的区别?

(第28章)Linux系统编程之文件与IO_其他_15

3.eg:调用 open(“somefile”, O_WRONLY|O_CREAT, 0664);

(第28章)Linux系统编程之文件与IO_#include_16
(第28章)Linux系统编程之文件与IO_库函数_17

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

(1)源码如下

#include <unistd.h>
int close(int fd);
返回值:成功返回0,出错返回-1并设置errno

(2)解释说明如下
(第28章)Linux系统编程之文件与IO_文件描述符_18

5. open 返回的文件描述符是该进程尚未使用的最小描述符,程序启动打开的描述符0,1,2

(第28章)Linux系统编程之文件与IO_文件描述符_19

6.具体eg

摘自:Unix系统编程第4章:文件I / O:通用I / O模型 fileio / copy.c(清单4-1)
http://man7.org/tlpi/code/online/book/fileio/copy.c

/*************************************************************************\
*                  Copyright (C) Michael Kerrisk, 2019.                   *
*                                                                         *
* This program is free software. You may use, modify, and redistribute it *
* under the terms of the GNU General Public License as published by the   *
* Free Software Foundation, either version 3 or (at your option) any      *
* later version. This program is distributed without any warranty.  See   *
* the file COPYING.gpl-v3 for details.                                    *
\*************************************************************************/

#include <sys/stat.h>
#include <fcntl.h>
#include "tlpi_hdr.h"

#ifndef BUF_SIZE        /* Allow "cc -D" to override definition */
#define BUF_SIZE 1024
#endif

int main(int argc, char *argv[])
{
    int inputFd, outputFd, openFlags;
    mode_t filePerms;
    ssize_t numRead;
    char buf[BUF_SIZE];

    if (argc != 3 || strcmp(argv[1], "--help") == 0)
        usageErr("%s old-file new-file\n", argv[0]);

    /* Open input and output files */

    inputFd = open(argv[1], O_RDONLY);
    if (inputFd == -1)
        errExit("opening file %s", argv[1]);

    openFlags = O_CREAT | O_WRONLY | O_TRUNC;
    filePerms = S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP |
                S_IROTH | S_IWOTH;      /* rw-rw-rw- */
    outputFd = open(argv[2], openFlags, filePerms);
    if (outputFd == -1)
        errExit("opening file %s", argv[2]);

    /* Transfer data until we encounter end of input or an error */

    while ((numRead = read(inputFd, buf, BUF_SIZE)) > 0)
        if (write(outputFd, buf, numRead) != numRead)
            fatal("couldn't write whole buffer");
    if (numRead == -1)
        errExit("read");

    if (close(inputFd) == -1)
        errExit("close input");
    if (close(outputFd) == -1)
        errExit("close output");

    exit(EXIT_SUCCESS);
}
四、read/write函数

1.read函数:从打开的设备或文件中读取数据

(1)
(第28章)Linux系统编程之文件与IO_其他_20

  • 参数count是请求读取的字节数,读上来的数据保存在缓冲区buf中,同时文件的当前读写位置向后移;
  • 这个读写位置是记在内核中的,而使用C标准I/O库时的读写位置是用户空间I/O缓冲区中的位置,eg:如用fgetc读一个字节, fgetc有可能从内核中预读1024个字节到I/O缓冲区中,再返回第一个字节,这时该文件在内核中记录的读写位置是1024,而在FILE结构体中记录的读写位置是1
  • 注意返回值类型是ssize_t,表示有符号的size_t, 这样既可以返回正的字节数、 0(表示到达文件末尾)也可以返回负值-1(表示出错);
  • read函数返回时,返回值说明了buf中前多少个字节是刚读上来的;
  • 实际读到的字节数(返回值)会小于请求读的字节数count,例如:
    (第28章)Linux系统编程之文件与IO_其他_21
    (2)读常规文件是不会阻塞的,不管读多少字节, read一定会在有限的时间内返回。
    从终端设备或网络读则不一定,如果从终端输入的数据没有换行符,调用read读终端设备就会阻塞,如果网络上没有接收到数据包,调用read从网络读就会阻塞,至于会阻塞多长时间也是不确定的,如果一直没有数据到达就一直阻塞在那里。

2.write函数:向打开的设备或文件中写数据

(1)
(第28章)Linux系统编程之文件与IO_文件描述符_22

  • 写常规文件时, write的返回值通常等于请求写的字节数count,而向终端设备或网络写则不一定。

(2)写常规文件是不会阻塞的,而向终端设备或网络写则不一定。

3.睡眠sleep,运行running

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

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

  • 正在被调度执行
    (第28章)Linux系统编程之文件与IO_文件描述符_23
  • 就绪状态
    (第28章)Linux系统编程之文件与IO_#include_24

4.阻塞读终端

0、 1、 2文件描述符上自动打开的文件就是终端

  • 下面这个小程序从终端读数据再写回终端
  • 程序开始执行时在0、 1、 2文件描述符上自动打开的文件就是终端,但是没O_NONBLOCK标志,所以 “阻塞读终端”的读标准输入是阻塞的
    (第28章)Linux系统编程之文件与IO_文件描述符_25
    解释如下:
    (第28章)Linux系统编程之文件与IO_文件描述符_26

5.非阻塞读终端,轮询Poll

(1)
(第28章)Linux系统编程之文件与IO_文件描述符_27
(2)非组赛IO的缺点
(第28章)Linux系统编程之文件与IO_文件描述符_28
(3)非阻塞I/O的eg1

  • 以下是一个非阻塞I/O的例子。目前我们学过的可能引起阻塞的设备只有终端,所以我们用终端来做这个实验。我们可以重新打开一遍设备文件/dev/tty(表示当前终端),在打开时指定O_NONBLOCK标志
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <string.h>
#include <stdlib.h>
#define MSG_TRY "try again\n"
int main(void)
{
	char buf[10];
	int fd, n;
	fd = open("/dev/tty", O_RDONLY|O_NONBLOCK);
	if(fd<0) 
	{
		perror("open /dev/tty");
		exit(1);
	}
tryagain:
	n = read(fd, buf, 10);
	if (n < 0) 
	{
		if (errno == EAGAIN) 
		{
			sleep(1);
			write(STDOUT_FILENO, MSG_TRY, strlen(MSG_TRY));
			goto tryagain;
		}
		perror("read /dev/tty");
		exit(1);
	}
	write(STDOUT_FILENO, buf, n);
	close(fd);
	return 0;
}

结果:
try again
try again
try again
try again
try again
try again
try again
try again
try again
try again
try again
try again
try again
try again
try again
try again
try again
try again
try again
try again
try again
try again
try again
try again
try again
try again
try again

(4)非阻塞I/O的eg2

  • 以下是用非阻塞I/O实现等待超时的例子。既保证了超时退出的逻辑又保证了有数据到达时处理延迟较小。
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <string.h>
#include <stdlib.h>
#define MSG_TRY "try again\n"
#define MSG_TIMEOUT "timeout\n"
int main(void)
{
	char buf[10];
	int fd, n, i;
	fd = open("/dev/tty", O_RDONLY|O_NONBLOCK);
	if(fd<0) 
	{
		perror("open /dev/tty");
		exit(1);
	}
	for(i=0; i<5; i++) 
	{
		n = read(fd, buf, 10);
		if(n>=0)
			break;
		if(errno!=EAGAIN) 
		{
			perror("read /dev/tty");
			exit(1);
		}
		sleep(1);
		write(STDOUT_FILENO, MSG_TRY, strlen(MSG_TRY));
	}
	if(i==5)
		write(STDOUT_FILENO, MSG_TIMEOUT, strlen(MSG_TIMEOUT));
	else
		write(STDOUT_FILENO, buf, n);
	close(fd);
	return 0;
}

结果:
try again
try again
try again
try again
try again
timeout

五、lseek:移动当前读写位置

(1)

  • 每个打开的文件都记录着当前读写位置,打开文件时读写位置是0,表示文件开头,通常读写多少个字节就会将读写位置往后移多少个字节
  • 但是有一个例外,如果以O_APPEND方式打开,每次写操作都会在文件末尾追加数据,然后将读写位置移到新的文件末尾。 lseek和标准I/O库的fseek函数类似,可以移动当前读写位置(或者叫偏移量)
    (第28章)Linux系统编程之文件与IO_文件描述符_29
  • 参数offset和whence的含义和fseek函数完全相同。只不过第一个参数换成了文件描述符。
  • 和fseek一样,偏移量允许超过文件末尾, 这种情况下对该文件的下一次写操作将延长文件,中间空洞的部分读出来都是0

(2)若lseek成功执行,则返回新的偏移量,因此可用以下方法确定一个打开文件的当前偏移量:
(第28章)Linux系统编程之文件与IO_其他_30

六、fcntl:改变已打开的文件的属性

1.基本用法

(1)先前我们以read终端设备为例介绍了非阻塞I/O,为什么我们不直接对STDIN_FILENO做非阻塞read,而要重新open一遍/dev/tty呢?
因为STDIN_FILENO在程序启动时已经被自动打开了,而我们需要在调用open时指定O_NONBLOCK标志。

(2)
(第28章)Linux系统编程之文件与IO_文件描述符_31

  • 可以用fcntl函数改变一个已打开的文件的属性,可以重新设置读、写、追加、非阻塞等标志(这些标志称为File Status Flag),而不必重新open文件。
  • 这个函数和open一样,也是用可变参数实现的,可变参数的类型和个数取决于前面的cmd参数

(3)eg1

  • 下面的例子使用F_GETFL和F_SETFL这两种fcntl命令改变STDIN_FILENO的属性,加上O_NONBLOCK选项,实现和 “非阻塞读终端”同样的功能。
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <string.h>
#include <stdlib.h>
#define MSG_TRY "try again\n"
int main(void)
{
	char buf[10];
	int n;
	int flags;
	flags = fcntl(STDIN_FILENO, F_GETFL);
	flags |= O_NONBLOCK;
	if (fcntl(STDIN_FILENO, F_SETFL, flags) == -1) 
	{
		perror("fcntl");
		exit(1);
	}
tryagain:
	n = read(STDIN_FILENO, buf, 10);
	if (n < 0) 
	{
		if (errno == EAGAIN) 
		{
			sleep(1);
			write(STDOUT_FILENO, MSG_TRY, strlen(MSG_TRY));
			goto tryagain;
		}
		perror("read stdin");
		exit(1);
	}
	write(STDOUT_FILENO, buf, n);
	return 0;
}

(4)eg2

  • 以下程序通过命令行的第一个参数指定一个文件描述符,同时利用Shell的重定向功能在该描述符上打开文件,然后用 fcntl 的 F_GETFL 命令取出File Status Flag并打印。
#include <sys/types.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char *argv[])
{
	int val;
	if (argc != 2) 
	{
		fputs("usage: a.out <descriptor#>\n", stderr);
		exit(1);
	}
	if ((val = fcntl(atoi(argv[1]), F_GETFL)) < 0) 
	{
		printf("fcntl error for fd %d\n", atoi(argv[1]));
		exit(1);
	}
	switch(val & O_ACCMODE) 
	{
		case O_RDONLY:
			printf("read only");
			break;
		case O_WRONLY:
			printf("write only");
			break;
		case O_RDWR:
			printf("read write");
			break;
		default:
			fputs("invalid access mode\n", stderr);
			exit(1);
	}
	if (val & O_APPEND)
		printf(", append");
	if (val & O_NONBLOCK)
		printf(", nonblocking");
	putchar('\n');
	return 0;
}

文件句柄(文件描述符)为0,1,2很特殊,所以下面用了三种情况分别表述
第一种运行该程序的情况:
(第28章)Linux系统编程之文件与IO_#include_32
说明如下:

  • C 库函数 int atoi(const char *str) 把参数 str 所指向的字符串转换为一个整数(类型为 int 型)
  • Shell在执行 a.out 时将它的标准输入重定向到 /dev/tty , 并且是只读的。 argv[1] 是0,因此取出文件描述符0(也就是标准输入) 的File Status Flag
  • 用掩码 O_ACCMODE 取出它的读写位,结果是 O_RDONLY
  • 注意,Shell的重定向语法不属于程序的命令行参数,这个命行只有两个参数, argv[0] 是"./a.out", argv[1] 是"0",重定向由Shell解释,在启动程序时已经生效,程序在运行时并不知道标准输入被重定向了

第二种运行该程序的情况:
(第28章)Linux系统编程之文件与IO_文件描述符_33
第三种运行该程序的情况:
(第28章)Linux系统编程之文件与IO_其他_34
(第28章)Linux系统编程之文件与IO_库函数_35

2.shell重定向语法:在<、>、>>、<>前面添一个数字

(1)我们看到一种新的Shell重定向语法,如果在<、>、>>、<>前面添一个数字,该数字就表示在哪个文件描述符上打开文件。
eg:

  • 例如2>>temp.foo表示将标准错误输出重定向到文件temp.foo并且以追加方式写入文件,注意2和>>之间不能有空格,否则2就被解释成命令行参数了;

(2)文件描述符数字还可以出现在重定向符号右边,例如:
(第28章)Linux系统编程之文件与IO_其他_36

3.fcntl的其他特点

(第28章)Linux系统编程之文件与IO_库函数_37

七、ioctl

1.out of band数据与in band数据

(1)

  • ioctl 用于向设备发控制和配置命令,有些命令也需要读写一些数据,但这些数据是不能用 read / write 读写的,称为Out-of-band数据
  • read / write 读写的数据是in band数据,是I/O操作的主体,而 ioctl 命令传送的是控制信息,其中的数据是辅助的数据

(2)eg:
例如,在串口线上收发数据通过 read / write 操作,而串口的波特率、校验位、停止位通过 ioctl 设置,A/D转换的结果通过 read 读取,而A/D转换的精度和工作频率通过 ioctl 设置。

2.基本用法

(1)
(第28章)Linux系统编程之文件与IO_文件描述符_38
(2)eg:在图形界面的终端里多次改变终端窗口的大小并运行该程序,观察结果

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/ioctl.h>
int main(void)
{
	struct winsize size;
	if (isatty(STDOUT_FILENO) == 0)
	exit(1);
	if(ioctl(STDOUT_FILENO, TIOCGWINSZ, &size)<0) 
	{
		perror("ioctl TIOCGWINSZ error");
		exit(1);
	}
	
	printf("%d rows, %d columns\n", size.ws_row, size.ws_col);
	return 0;
}

结果如下:
49 rows, 127 columns
再次改变终端窗口大小,得到
42 rows, 114 columns

八、mmp:将磁盘文件的一部分直接映射到内存

(1)基本用法

  • mmap 可以把磁盘文件的一部分直接映射到内存,这样文件中的位置直接就有对应的内存地址,对文件的读写可以直接用指针来做而不需要 read / write 函数
  • 如果 mmap 成功则返回映射首地址,如果出错则返回常数 MAP_FAILED 。当进程终止时,该进程的映射内存会自动解除,也可以调用 munmap 解除映射。 munmap 成功返回0,出错返回-1
    (第28章)Linux系统编程之文件与IO_#include_39
    该函数各参数的作用图示如下:
    (第28章)Linux系统编程之文件与IO_文件描述符_40
  • 参数:addr
    (第28章)Linux系统编程之文件与IO_其他_41
  • 参数:len,len 参数是需要映射的那一部分文件的长度
  • 参数:prot,
    (第28章)Linux系统编程之文件与IO_文件描述符_42
  • 参数:flag
    (第28章)Linux系统编程之文件与IO_库函数_43
  • 参数:filedes,filedes 是代表该文件的描述符
  • 参数:off,off 参数是从文件的什么位置开始映射,必须是页大小的整数倍(在32位体系统结构上通常是4K)

(2)eg:
(第28章)Linux系统编程之文件与IO_其他_44

  • 现在用如下程序操作这个文件(注意,把 fd 关掉并不影响该文件已建立的映射,仍然可以对文件进行读写)
#include <stdlib.h>
#include <sys/mman.h>
#include <fcntl.h>
int main(void)
{
	int *p;
	int fd = open("hello", O_RDWR);
	if (fd < 0) 
	{
		perror("open hello");
		exit(1);
	}
	p= mmap(NULL, 6, PROT_WRITE, MAP_SHARED, fd, 0);
	if (p == MAP_FAILED) 
	{
		perror("mmap");
		exit(1);
	}
  close(fd);
	p[0] = 0x30313233;
	munmap(p, 6);
	return 0;
}

(第28章)Linux系统编程之文件与IO_#include_45
eg2:一个很简单的hello world程序:

  • mmap 函数的底层也是一个系统调用,在执行程序时经常要用到这个系统调用来映射共享库到该进程的地址空间
#include <stdio.h>
int main(void)
{
	printf("hello world\n");
	return 0;
}

用 strace 命令执行该程序,跟踪该程序执行过程中用到的所有系统调用的参数及返回值:
(第28章)Linux系统编程之文件与IO_#include_46