131 Linux 系统编程8 IO操作 系统函数 open,close, read/write,strerrno,perror,exit,_exit,strace,EAGAIN, fcntl,位图
一 系统调用
什么是系统调用:
由操作系统实现并提供给外部应用程序的编程接口。(Application Programming Interface,API)。是应用程序同系统之间数据交互的桥梁。
C标准函数和系统函数调用关系。一个helloworld如何打印到屏幕。

二 回忆我们前面学过的C标准库文件IO函数。
fopen、fclose、fseek、fgets、fputs、fread、fwrite......
r 只读、 r+读写
w只写并截断为0、 w+读写并截断为0
a追加只写、 a+追加读写
三 系统函数
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);
函数原型:
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
int close(int fd);
常用参数
O_RDONLY、O_WRONLY、O_RDWR
O_APPEND、 在文件末尾追加
O_CREAT、 创建文件
O_EXCL、
O_TRUNC、 清空
O_NONBLOCK
创建文件时,指定文件访问权限。权限同时受umask影响。结论为:
文件权限 = mode & ~umask
使用头文件:<fcntl.h>
open常见错误 :
1. 打开文件不存在
2. 以写方式打开只读文件(打开文件没有对应权限)
3. 以只写方式打开目录
返回值
返回一个 fd,是无符号整型,代表的是 文件描述符
如果有error,则返回-1
如果成功,返回对应该文件的fd,
正常 fd 是3开始的,因为0,1,2 被占用了。
文件描述符存在于 PCB 进程控制块中。PCB 进程控制块存在于

举例
#include <cstdio>
#include <unistd.h>
#include <fcntl.h>
#include <iostream>
#include <errno.h>
#include <string.h>
using namespace std;int main()
{int fd;//fd = open("/home/hunandede/projects/linuxcpp/dict.txt",O_RDONLY);fd = open("./c.txt", O_RDONLY);//打开当前可执行程序 同一级 目录下的 a.txt文件if (fd==-1) {cout << "can not find dict.txt fd = " << fd << " errno = " << errno << " strerror(errno) = " << strerror(errno) << endl;strerror(errno);}int fd2;fd2 = open("./b.txt", O_CREAT | O_RDWR, 0664);cout << "fd2 = " << fd2 << endl;if (fd2<0) {cout << "crate b.txt fail fd2 = " << fd2 << " errno = " << errno << endl;}printf("hello from linuxcpp!\n");return 0;
}
read/write 函数
read函数原型:
ssize_t read(int fd, void *buf, size_t count);
参数:
fd:文件描述符,从那个文件里面读取
buf:读到的数据放到哪里(缓冲区)
count:缓冲区大小,
返回值 ssize_t ,long int
如果成功则返回读取到的字节数,如果不成功则返回-1.读到文件末尾则返回0
如果返回-1,还需要判断 errno = EAGIN 或 EWOULDBLOCK,说明不是read失败,而是read在读一个设备文件 或者 网络
这和后面要学习的 阻塞相关。
当我们读取一个普通文件的时候,不会发生阻塞,但是当我们读取一个 设备文件的时候,比如 输入或者输出设备,例如键盘,或者 显示屏,这时候你不在键盘上输入字符,键盘这个输入设备就会阻塞,
当我们从网络上读取数据的时候,如果网络不稳定,或者直接就断网了,那么也会阻塞。
在这种case下,如果我们调用open函数的时候,加上 O_NONBLOCK ,也就是说:read是以非阻塞的方式读取文件,这时候如果设备 或者网络阻塞了,就会返回 errno = EAGIN 或 EWOULDBLOCK
对于大多数系统,EAGAIN和EWOULDBLOCK将是相同的。只有少数系统不同,可以忽略。
write函数原型:
ssize_t write(int fd, const void *buf, size_t count);
参数
fd:文件描述符,给那个文件里面写
buf:将哪里的数据写入fd (原材料)
count:数据的大小
返回值
成功时,返回写入的数据大小,返回0说明没有任何数据写入。返回负值说明有问题发生。
On success, the number of bytes written is returned (zero indicates
nothing was written). It is not an error if this number is smaller
than the number of bytes requested; this may happen for example because
the disk device was filled. See also NOTES.
On error, -1 is returned, and errno is set appropriately.
read与write函数原型类似。使用时需注意:read/write函数的第三个参数。
例子
//实现linux cp命令,只是copy一个源文件
//cp a.txt b.txt
int main(int argc, char * argv[]) {cout << "argc = " << argc << endl;for (size_t i = 0; i < argc; i++){cout << "argv[" << i <<"] = " << argv[i] << endl;}int ret = 0;//1.打开源文件int fd1 = open(argv[1],O_RDONLY);if (fd1==-1) {cout << "open " <<argv[1] <<" file error,errno = "<< errno << " because "<< strerror(errno) << endl;ret = fd1;return ret;}//2.创建新文件int fd2 = open(argv[2],O_RDWR|O_CREAT|O_TRUNC,0664);if (fd2 == -1) {cout << "create OR trunc" << argv[2] << " file error,errno = " << errno << " because " << strerror(errno) << endl;ret = fd2;return ret;}//3.从源文件中读取,读取的内容存放在buf中,结果为读取到的字节数,当readlen==0的时候,说明读取到了末尾,char buf[1024] = {0};int readlen = 0;while ((readlen = read(fd1,buf,1024)) != 0) {//注意,这里,readlen 是读取到的数据,>0表示成功读取到了, ==0表示读取到了文件末尾,还有可能<0,小于0说明发生了问题有errorif (readlen < 0) {cout << "read " << argv[1] << " data has been error readlen = " << readlen << " beucase " << strerror(errno) << endl;ret = readlen;return ret;}//到这里说明,就是readlen>0了,也就是成功的读取到数据了,那么我们就可以写入了write(fd2, buf, readlen);}close(fd1);close(fd2);
}
四 错误处理函数:
char* strerrno(int errnum ))
| 在标头 | ||
| char* strerror( int errnum ); |
例子 : cout<<"err info "<< strerror(errno)<<endl;
void perror (const char *s);
| 在标头 |
| void perror( const char *s ); |
打印当前存储于系统变量 errno 的错误码到 stderr 。
例子: perror("描述错误,并将当前的errno 打印出来");
五 退出当前进程函数 exit _exit
这个是 标准库函数,并不是系统函数
#include <stdlib.h>
void exit(int status);
例子: exit(1)
系统函数
_exit 系统函数
#include <unistd.h>
void _exit(int status);
#include <stdlib.h>
void _Exit(int status);
六 strace命令 查看该可执行文件用到的系统函数
shell中使用strace命令跟踪程序执行,查看调用的系统函数。
strace ./a.out
七。缓冲区的概念
我们在使用标准库函数的时候,例如:fputc 和 fgetc 这样的 库函数。
虽然这个库函数是的意思是一个一个的读取,或者一个一个的写入,
但是实际上在user 区有一个buffer 的,一般是4096 大小。这个对程序员是隐藏的。
当我们使用 系统函数 read ,write的时候,当我们设置了缓冲区的大小是自己的缓冲区,并不是系统在用户级别偷偷设置的缓冲区。
read、write函数常常被称为Unbuffered I/O。指的是无用户级缓冲区。但不保证不使用内核缓冲区。

注意的是:当从user 的数据写入kernel的时候,kernel也有一个缓冲区,这个缓冲区的大小是4096,当这个缓冲区写满的时候,才会真正的写入磁盘。
结论:我们即使学习了系统函数,能用标准库函数,还是优先使用标准库函数。
八,PCB进程控制块 和 文件描述符
从上述的学习我们知道了 open 函数 会返回一个文件描述符。
那么这个文件描述符在程序层面上,是定义在哪里的呢?
大前提:很显然,我们的系统调用是kernel 空间,那么具体在哪里呢?
从下图我们看到 文件描述符 是在 kernel 层面的,PCB进程块中的。
PCB 在整个程序运行中的位置,以及结构
process control block

可使用命令locate sched.h查看位置: /usr/src/linux-headers-3.16.0-30/include/linux/sched.h
struct task_struct { 结构体
.....
}
文件描述符表
结构体PCB 的成员变量file_struct *file 指向文件描述符表。
从应用程序使用角度,该指针可理解记忆成一个字符指针数组,下标0/1/2/3/4...找到文件结构体。
本质是一个键值对0、1、2...都分别对应具体地址。但键值对使用的特性是自动映射,我们只操作键不直接使用值。
新打开文件返回文件描述符表中未使用的最小文件描述符。
STDIN_FILENO 0
STDOUT_FILENO 1
STDERR_FILENO 2
最大打开文件数
一个进程默认打开文件的个数1024。
命令查看ulimit -a 查看open files 对应值。默认为1024
可以使用ulimit -n 4096 修改
当然也可以通过修改系统配置文件永久修改该值,但是不建议这样操作。
cat /proc/sys/fs/file-max可以查看该电脑最大可以打开的文件个数。受内存大小影响。
FILE结构体
主要包含文件描述符、文件读写位置、IO缓冲区三部分内容。
struct file {
...
文件的偏移量;
文件的访问权限;
文件的打开标志;
文件内核缓冲区的首地址;
struct operations * f_op;
...
};
查看方法:
(1) /usr/src/linux-headers-3.16.0-30/include/linux/fs.h
(2) lxr:百度 lxr → lxr.oss.org.cn → 选择内核版本(如3.10) → 点击File Search进行搜索
→ 关键字:“include/linux/fs.h” → Ctrl+F 查找 “struct file {”
→ 得到文件内核中结构体定义
→ “struct file_operations”文件内容操作函数指针
→ “struct inode_operations”文件属性操作函数指针
九。阻塞和非阻塞
当我们读取一个普通文件的时候,不会发生阻塞,但是当我们读取一个 设备文件的时候,比如 输入或者输出设备,例如键盘,或者 显示屏,这时候你不在键盘上输入字符,键盘这个输入设备就会阻塞,
当我们从网络上读取数据的时候,如果网络不稳定,或者直接就断网了,那么也会阻塞。
在这种case下,如果我们调用open函数的时候,加上 O_NONBLOCK ,也就是说:read是以非阻塞的方式读取文件,这时候如果设备 或者网络阻塞了,就会返回 errno = EAGIN 或 EWOULDBLOCK
对于大多数系统,EAGAIN和EWOULDBLOCK将是相同的。只有少数系统不同,可以忽略。
例子:
int main() {int ret = 0;//1.读取键盘设备,键盘设备在linux下对应的文件是 "dev/tty"int fd1 = open("/dev/tty", O_RDONLY|O_NONBLOCK);if (fd1 == -1) {cout << "open /dev/tty FAIL error,errno = " << errno << " because " << strerror(errno) << endl;ret = fd1;return ret;}char buf[1024] = {0};int realreadbuflen;tryagain:realreadbuflen = read(fd1,buf,1024);if (realreadbuflen < 0) {//如果是non_block打开文件,且我们读取的是设备或者网络,当前读取的是 键盘设备,且block的时候, 要判断 errno的值是否是EAGAIN或者EWOULDBLOCKif (errno == EAGAIN || errno == EWOULDBLOCK) {//查看源码,在ubutun上 这两是一个值,因此判断一个就行,但是这里按照自己的编码风格还是写了两个 #define EWOULDBLOCK EAGAIN /* Operation would block *///走到这里。说明不是真的有问题,因此要提示user,然后让user输入数据write(STDOUT_FILENO, "please insert data\n", strlen("please insert data\n"));sleep(2);goto tryagain;}else {perror("read /dev/tty error\n");exit(errno);}}write(STDOUT_FILENO,buf, realreadbuflen);close(fd1);return 0;
}
十 fcntl函数
fcntl函数异常复杂,可以传递的参数种类很多,我们这里学习的是通过fcntl函数改变 文件属性的代码。
我们来看上述代码的缺陷,在open "/dev/tty" 的时候,是需要重新打开文件的。这里学习的就是通过fcntl函数不用重新打开文件就能改变 文件的属性,例如,刚开始打开的时候没有 O_NONBLOCK属性,
函数原型
int flag = fcntl(STDIN_FILENO,F_GETFL);
得到STDIN_FILENO的属性
参数:
int fd,
int F_GETFL
返回值:
-1 means error
正常则返回 fd 的属性
int ret= fcntl(STDIN_FILENO,F_SETFL,flag | O_NONBLOCK);
参数:
int fd
int F_SETFL
int flag
返回值:
-1 means error ,errno中存储的问题发生的代码
成功返回0,新的属性已经设置进去了。
例子:
int main() {int ret = 0;//1.读取键盘设备,键盘设备在linux下对应的文件是 "dev/tty",//注意开始的时候,并没有 O_NONBLOCK 属性int fd1 = open("/dev/tty", O_RDONLY);if (fd1 == -1) {cout << "open /dev/tty FAIL error,errno = " << errno << " because " << strerror(errno) << endl;ret = fd1;return ret;}int flag = fcntl(fd1,F_GETFL);if (flag == -1) {perror("get /dev/tty F_GETFL err");ret = flag;exit(flag);}flag = flag | O_NONBLOCK;ret = fcntl(fd1, F_SETFL, flag);if (ret == -1) {perror("set /dev/tty flag | O_NONBLOCK err");exit(ret);}char buf[1024] = { 0 };int realreadbuflen;
tryagain:realreadbuflen = read(fd1, buf, 1024);if (realreadbuflen < 0) {//如果是non_block打开文件,且我们读取的是设备或者网络,当前读取的是 键盘设备,且block的时候, 要判断 errno的值是否是EAGAIN或者EWOULDBLOCKif (errno == EAGAIN || errno == EWOULDBLOCK) {//查看源码,在ubutun上 这两是一个值,因此判断一个就行,但是这里按照自己的编码风格还是写了两个 #define EWOULDBLOCK EAGAIN /* Operation would block *///走到这里。说明不是真的有问题,因此要提示user,然后让user输入数据write(STDOUT_FILENO, "please insert data\n", strlen("please insert data\n"));sleep(2);goto tryagain;}else {perror("read /dev/tty error\n");exit(errno);}}write(STDOUT_FILENO, buf, realreadbuflen);close(fd1);return 0;
}
而且我们发现,fd1实际上已经没有用了,还可以改动为:
int main() {int ret = 0;//直接使用STDIN_FILENO,不要fd1了int flag = fcntl(STDIN_FILENO,F_GETFL);if (flag == -1) {perror("get /dev/tty F_GETFL err");ret = flag;exit(flag);}flag = flag | O_NONBLOCK;ret = fcntl(STDIN_FILENO, F_SETFL, flag);if (ret == -1) {perror("set /dev/tty flag | O_NONBLOCK err");exit(ret);}char buf[1024] = { 0 };int realreadbuflen;
tryagain:realreadbuflen = read(STDIN_FILENO, buf, 1024);if (realreadbuflen < 0) {//如果是non_block打开文件,且我们读取的是设备或者网络,当前读取的是 键盘设备,且block的时候, 要判断 errno的值是否是EAGAIN或者EWOULDBLOCKif (errno == EAGAIN || errno == EWOULDBLOCK) {//查看源码,在ubutun上 这两是一个值,因此判断一个就行,但是这里按照自己的编码风格还是写了两个 #define EWOULDBLOCK EAGAIN /* Operation would block *///走到这里。说明不是真的有问题,因此要提示user,然后让user输入数据write(STDOUT_FILENO, "please insert data 111 \n", strlen("please insert data 111 \n"));sleep(2);goto tryagain;}else {perror("read /dev/tty error\n");exit(errno);}}write(STDOUT_FILENO, buf, realreadbuflen);return 0;
}
十一 :位图的概念
后边经常会用到,使用一个数字,该数字的每一位 都代表一个属性。
