0.目录
1.系统调用
2.open/close函数
3.文件描述符
4.read/write函数
5.错误处理函数
6.阻塞、非阻塞
7.lseek函数
8.fcntl函数
9.ioctl函数
10.传入传出参数
1.系统调用
什么是系统调用:
由操作系统实现并提供给外部应用程序的编程接口。(Application Programming Interface,API)。是应用程序同系统之间数据交互的桥梁。
C标准函数和系统函数调用关系。一个helloworld如何打印到屏幕。
1.1C标准库文件IO函数。
fopen、fclose、fseek、fgets、fputs、fread、fwrite......
r 只读、 r+读写
w只写并截断为0、 w+读写并截断为0
a追加只写、 a+追加读写
2.open/close函数
2.1函数原型
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
int close(int fd);
//mode传的是数字权限 例如0777,0644,但是是八进制
//三个参数的方法只有在第二个参数中有O_CREAT作为参数的时候才需要使用
//linux系统编程取代了常规C语言IO的地方就是文件指针被替换成了一个整型的数字
2.2常用参数
O_RDONLY、O_WRONLY、O_RDWR //只读 只写 读写
O_APPEND、O_CREAT、O_EXCL、 O_TRUNC、 O_NONBLOCK //追加,(不存在就)创建,文件是否存在,将文件截断为0(清空) ,非阻塞
使用头文件:
2.3open常见错误
- 打开文件不存在
- 以写方式打开只读文件(打开文件没有对应权限)
- 以只写方式打开目录
2.4例子
#include
#include
#include
#include
int main(void)
{
int fd;
char buf[64];
int ret = 0;
fd = open("./file.txt", O_RDONLY);
if (fd == -1) {
printf("open file error\n");
exit(1);
}
printf("---open ok---\n");
ret = read(fd,buf,sizeof(buf));
while(ret) {
write(fd, buf, ret);
ret = read(fd,buf,sizeof(buf));
}
close(fd);
return 0;
}
3.文件描述符
3.1PCB进程控制块
可使用命令locate sched.h查看位置: /usr/src/linux-headers-3.16.0-30/include/linux/sched.h
3.2文件描述符表
结构体PCB 的成员变量file_struct *file 指向文件描述符表。
从应用程序使用角度,该指针可理解记忆成一个字符指针数组,下标0/1/2/3/4...找到文件结构体。
本质是一个键值对0、1、2...都分别对应具体地址。但键值对使用的特性是自动映射,我们只操作键不直接使用值。
新打开文件返回文件描述符表中未使用的最小文件描述符。
STDIN_FILENO 0
STDOUT_FILENO 1
STDERR_FILENO 2
3.2FILE结构体
主要包含文件描述符、文件读写位置、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”文件属性操作函数指针
3.3最大打开文件数
一个进程默认打开文件的个数1024。
命令查看unlimit -a 查看open files 对应值。默认为1024
可以使用ulimit -n 4096 修改
当然也可以通过修改系统配置文件永久修改该值,但是不建议这样操作。
cat /proc/sys/fs/file-max可以查看该电脑最大可以打开的文件个数。受内存大小影响。
4.read/write函数
ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);
read与write函数原型类似。使用时需注意:read/write函数的第三个参数。
count在read和write中不尽相同,请看2.4例子
4.1编写程序实现简单的cp功能
#include
#include
#include
#include
#include
#include
#include
#define N 1 //1024
int main(int argc, char *argv[])
{
int fd, fd_out;
int n;
char buf[N];
clock start,end;
start = clock();
fd = open(argv[1], O_RDONLY);//argv[0]是./app
if(fd < 0){
perror("open dict.txt error");
exit(1);
}
fd_out = open(argv[2], O_WRONLY|O_CREAT|O_TRUNC, 0644);
if(fd < 0){
perror("open dict.cp error");
exit(1);
}
while((n = read(fd, buf, N))){
if(n < 0){
perror("read error");
exit(1);
}
write(fd_out, buf, n);
}
end = clock();
printf("time:%ld",(double)(end-start));
close(fd);
close(fd_out);
return 0;
}
4.2程序比较
如果一个只读一个字节实现文件拷贝,使用read、write效率高,还是使用对应的标库函数效率高呢?
#include
#include
#include
int main(void)
{
FILE *fp, *fp_out;
int n;
clock start,end;
start = clock();
fp = fopen("dict.txt", "r");
if(fp == NULL){
perror("fopen error");
exit(1);
}
fp_out = fopen("dict.cp", "w");
if(fp == NULL){
perror("fopen error");
exit(1);
}
while((n = fgetc(fp)) != EOF){
fputc(n, fp_out);
}
end = clock();
printf("time:%ld",(double)(end-start));
fclose(fp);
fclose(fp_out);
return 0;
}
比较可知识fopen/fclose效率明显高于read/write,为什么?先来看一副图
- 用户程序直接使用read/write函数时,因为write是底层函数,调用过程式把用户态转变成内核态,而随着这种状态的转变,时间的消耗也是在增加的。
- 而调用标库函数fgetc/fputc时,函数会把数据调进一个缓冲区(大小4096k)中,再write改变系统状态发到内核进行操作,所以效率会有很大的不同
4.3strace命令
shell中使用strace命令跟踪程序执行,查看调用的系统函数。
strace ./app
4.4缓冲区
read、write函数常常被称为Unbuffered I/O。指的是无用户及缓冲区。但不保证不使用内核缓冲区。
5.错误处理函数
错误号:errno
perror函数: void perror(const char *s);
strerror函数: char *strerror(int errnum);
perror(“open error”);//自动补全错误信息
printf(“open error:%s\n”,strerror(errno));//把错误编号转换成字符
查看错误号:
/usr/include/asm-generic/errno-base.h
/usr/include/asm-generic/errno.h
6.阻塞、非阻塞
读常规文件是不会阻塞的,不管读多少字节,read一定会在有限的时间内返回。从终端设备或网络读则不一定,如果从终端输入的数据没有换行符,调用read读终端设备就会阻塞,如果网络上没有接收到数据包,调用read从网络读就会阻塞,至于会阻塞多长时间也是不确定的,如果一直没有数据到达就一直阻塞在那里。同样,写常规文件是不会阻塞的,而向终端设备或网络写则不一定。
现在明确一下阻塞(Block)这个概念。当进程调用一个阻塞的系统函数时,该进程被置于睡眠(Sleep)状态,这时内核调度其它进程运行,直到该进程等待的事件发生了(比如网络上接收到数据包,或者调用sleep指定的睡眠时间到了)它才有可能继续运行。与睡眠状态相对的是运行(Running)状态,在Linux内核中,处于运行状态的进程分为两种情况:
正在被调度执行。CPU处于该进程的上下文环境中,程序计数器(eip)里保存着该进程的指令地址,通用寄存器里保存着该进程运算过程的中间结果,正在执行该进程的指令,正在读写该进程的地址空间。
就绪状态。该进程不需要等待什么事件发生,随时都可以执行,但CPU暂时还在执行另一个进程,所以该进程在一个就绪队列中等待被内核调度。系统中可能同时有多个就绪的进程,那么该调度谁执行呢?内核的调度算法是基于优先级和时间片的,而且会根据每个进程的运行情况动态调整它的优先级和时间片,让每个进程都能比较公平地得到机会执行,同时要兼顾用户体验,不能让和用户交互的进程响应太慢。
阻塞读终端: 【block_readtty.c】//不给我不走
非阻塞读终端 【nonblock_readtty.c】//不给我也走了
非阻塞读终端和等待超时 【nonblock_timeout.c】//不给我不走,除非时间到
注意,阻塞与非阻塞是对于文件而言的。而不是read、write等的属性。read终端,默认阻塞读。
总结read 函数返回值:
- 返回非零值: 实际read到的字节数
- 返回 -1:
1):errno != EAGAIN (或!=EWOULDBLOCK) read出错
2):errno == EAGAIN (或==EWOULDBLOCK) 设置了非阻塞读,并且没有数据到达。 - 返回0:读到文件末尾
6.1阻塞例子
block_readtty.c
#include
#include
#include
//hello worl d \n
int main(void)
{
char buf[10];
int n;
n = read(STDIN_FILENO, buf, 10); // #define STDIN_FILENO 0 STDOUT_FILENO 1 STDERR_FILENO 2
if(n < 0){
perror("read STDIN_FILENO");
//printf("%d", errno);
exit(1);
}
write(STDOUT_FILENO, buf, n);
return 0;
}
nonblock_readtty.c
#include
#include
#include
#include
#include
#include
#define MSG_TRY "try again\n"
int main(void)
{
char buf[10];
int fd, n;
fd = open("/dev/tty", O_RDONLY|O_NONBLOCK); //使用O_NONBLOCK标志设置非阻塞读终端
if(fd < 0){
perror("open /dev/tty");
exit(1);
}
tryagain:
n = read(fd, buf, 10); //-1 (1) 出错 errno==EAGAIN或者EWOULDBLOCK
if(n < 0){
//由于open时指定了O_NONBLOCK标志,read读设备,没有数据到达返回-1,同时将errno设置为EAGAIN或EWOULDBLOCK
if(errno != EAGAIN){ //也可以是 if(error != EWOULDBLOCK)两个宏值相同
perror("read /dev/tty");
exit(1);
}
sleep(3);
write(STDOUT_FILENO, MSG_TRY, strlen(MSG_TRY));
goto tryagain;
}
write(STDOUT_FILENO, buf, n);
close(fd);
return 0;
}
nonblock_timeout.c
#include
#include
#include
#include
#include
#include
#define MSG_TRY "try again\n"
#define MSG_TIMEOUT "time out\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);
}
printf("open /dev/tty ok... %d\n", fd);
for (i = 0; i < 5; i++){
n = read(fd, buf, 10);
if(n > 0){ //说明读到了东西
break;
}
if(errno != EAGAIN){ //EWOULDBLK
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;
}
7.lseek函数
Linux中可使用系统函数lseek(L seek)来修改文件偏移量(读写位置)
每个打开的文件都记录着当前读写位置,打开文件时读写位置是0,表示文件开头,通常读写多少个字节就会将读写位置往后移多少个字节。但是有一个例外,如果以O_APPEND方式打开,每次写操作都会在文件末尾追加数据,然后将读写位置移到新的文件末尾。lseek和标准I/O库的fseek函数类似,可以移动当前读写位置(或者叫偏移量)。
回忆fseek的作用及常用参数。 SEEK_SET、SEEK_CUR、SEEK_END
int fseek(FILE *stream, long offset, int whence); 成功返回0;失败返回-1
特别的:超出文件末尾位置返回0;往回超出文件头位置,返回-1
off_t lseek(int fd, off_t offset, int whence); 失败返回-1;成功:返回的值是较文件起始位置向后的偏移量。
特别的:lseek允许超过文件结尾设置偏移量,文件会因此被拓展(但是这个拓展必须有write或者read这样的IO操作)。
注意文件“读”和“写”使用同一偏移位置。 【lseek.c】
lseek.c
#include
#include
#include
#include
#include
int main(void)
{
int fd, n;
char msg[] = "It's a test for lseek\n";
char ch;
fd = open("lseek.txt", O_RDWR|O_CREAT, 0644);
if(fd < 0){
perror("open lseek.txt error");
exit(1);
}
write(fd, msg, strlen(msg)); //使用fd对打开的文件进行写操作,文件读写位置位于文件结尾处。
lseek(fd, 0, SEEK_SET); //修改文件读写指针位置,位于文件开头。
//注释上行会怎样呢?while循环无效了,因为指针在最后,读不出任何数据
while((n = read(fd, &ch, 1))){
if(n < 0){
perror("read error");
exit(1);
}
write(STDOUT_FILENO, &ch, n); //将文件内容按字节读出,写出到屏幕
}
close(fd);
return 0;
}
7.1 lseek常用应用
使用lseek拓展文件:write操作才能实质性的拓展文件。单lseek是不能进行拓展的。
一般:write(fd, "a", 1);
od -tcx filename 查看文件的16进制表示形式
od -tcd filename 查看文件的10进制表示形式通过lseek获取文件的大小:lseek(fd, 0, SEEK_END); 【lseek_test.c】
[最后注意]:lseek函数返回的偏移量总是相对于文件头而言。
lseek_test.c
#include
#include
#include
#include
#include
int main(void)
{
int fd;
fd = open("lseek.txt", O_RDONLY | O_CREAT, 0664);
if(fd < 0){
perror("open lseek.txt error");
exit(1);
}
int len = lseek(fd, 0, SEEK_END);//获取文件大小
if(len == -1){
perror("lseek error");
exit(1);
}
printf("len of msg = %d\n", len);
//int ret = truncate("lseek.txt", 1500);
int ret = ftruncate(fd, 1800);
if(ret == -1){
perror("ftrun error");
exit(1);
}
#if 0
len = lseek(fd, 999, SEEK_SET);
if(len == -1){
perror("lseek seek_set error");
exit(1);
}
int ret = write(fd, "a", 1);
if(ret == -1){
perror("write error");
exit(1);
}
#endif
#if 0
off_t cur = lseek(fd, -10, SEEK_SET);
printf("--------| %ld\n", cur);
if(cur == -1){
perror("lseek error");
exit(1);
}
#endif
close(fd);
return 0;
}
8.fcntl函数
改变一个【已经打开】的文件的 访问控制属性。
重点掌握两个参数的使用,F_GETFL 和 F_SETFL。
F_GETFL 获取文件访问控制属性
F_SETFL 设置文件访问控制属性
【fcntl.c】
fcntl.c
#include
#include
#include
#include
#include
#include
#define MSG_TRY "try again\n"
int main(void)
{
char buf[10];
int flags, n;
flags = fcntl(STDIN_FILENO, F_GETFL); //获取stdin属性信息
if(flags == -1){
perror("fcntl error");
exit(1);
}
flags |= O_NONBLOCK; //位或 让flags多了一个非阻塞属性 ---------图解!
int ret = fcntl(STDIN_FILENO, F_SETFL, flags);//设置属性
if(ret == -1){
perror("fcntl error");
exit(1);
}
/*
int fd = open("/dev/tty", O_RDONLY|O_NONBLOCK);以前 我们是这样设置STDIN_FILENO的属性的
现在 我们只需要在程序中 --先取flags 位或 设置 就可以做到了更改访问控制属性的效果
*/
tryagain:
n = read(STDIN_FILENO, buf, 10);
if(n < 0){
if(errno != EAGAIN){
perror("read /dev/tty");
exit(1);
}
sleep(3);
write(STDOUT_FILENO, MSG_TRY, strlen(MSG_TRY));
goto tryagain;
}
write(STDOUT_FILENO, buf, n);
return 0;
}
9.ioctl函数
对设备的I/O通道进行管理,控制设备特性。(主要应用于设备驱动程序中)。
通常用来获取文件的【物理特性】(该特性,不同文件类型所含有的值各不相同)
【ioctl.c】
ioctl.c
#include
#include
#include
#include
int main(void)
{
struct winsize size;
if (isatty(STDOUT_FILENO) == 0)
exit(1);
if(ioctl(STDOUT_FILENO, TIOCGWINSZ, &size)<0) { //获得当前窗口的数据,保存在size中!
perror("ioctl TIOCGWINSZ error");
exit(1);
}
printf("%d rows, %d columns\n", size.ws_row, size.ws_col);
return 0;
}
10.传入传出参数
10.1传入参数
const 关键字修饰的 指针变量 在函数内部读操作。
char *strcpy(const char *src, char *dst);
10.2传出参数
- 指针做为函数参数
- 函数调用前,指针指向的空间可以无意义,调用后指针指向的空间有意义,且作为函数的返回值传出
- 在函数内部写操作。
10.3传入传出参数
- 调用前指向的空间有实际意义
- 调用期间在函数内读、写(改变原值)操作
- 作为函数返回值传出。