Linux系统调用以及用户编程接口
三者关系
系统调用、API以及系统命令之间关系:
什么是文件描述符?
是一个非负整数,索引值
打开或者创建一个文件的时候,内核会向进程返回一个文件描述符
读写文件时,会向函数传递一个文件描述符
底层文件I/O操作
特点:不带缓存,直接对文件(设备)进行读写操作
基本文件操作(五个函数)
open():打开或者创建文件,可以指定文件属性以及用户权限 。
第一个参数 | 被打开文件名,可以包括路径 | ||||||||||
第二个参数 |
| ||||||||||
第三个参数 |
| ||||||||||
返回值 | 成功:返回文件描述符 失败:-1 |
close():关闭一个被打开的文件,成功返回0,失败返回-1
唯一参数 | fd:文件描述符 |
返回值 | 0:成功 -1:出错 |
read():把指定的文件描述符中读出的数据放入缓存区
第一个参数 | 文件描述符 |
第二个参数 | 指定读的缓存在哪 |
第三个参数 | 指定读的字节数 |
返回值 | 成功:已读字节数 失败:-1 到达文件尾部:0 |
write():向打开的文件写数据,从当前指针位置开始。
第一个参数 | 文件描述符 |
第二个参数 | 指定写的缓存在哪 |
第三个参数 | 指定写的字节数 |
返回值 | 成功:已写字节数 失败:-1 |
lseek():将文件指针定位到相应位置
第一个参数 | 文件描述符 | ||||||
第二个参数 | 偏移量,+向前移动,-向后移动 | ||||||
第三个参数 |
| ||||||
返回值 | 成功:文件的当前位移 失败:-1 |
示例代码:
/* copy_file.c */
/*头文件部分*/
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <stdio.h>
/*宏定义*/
#define BUFFER_SIZE 1024 /* 每次读写缓存大小,影响运行效率,也就是都多少写多少*/
#define SRC_FILE_NAME "src_file" /* 源文件名 */
#define DEST_FILE_NAME "dest_file" /* 目标文件名文件名 */
#define OFFSET 10240 /* 复制的数据大小,用来算偏移多少 */
int main()
{
/*首先定义一些整形变量和数组*/
int src_file, dest_file;/*整形变量用来存放文件描述符*/
unsigned char buff[BUFFER_SIZE];/*无符号字符数组,用来存储源文件src_file内容*/
int real_read_len; /*用来存储read返回的读到的字节数*/
/* 以只读方式打开源文件 ,为了之后读取文件,返回的文件描述符赋值给src_file*/
/*这里文件名字是宏定义*/
src_file = open(SRC_FILE_NAME, O_RDONLY);
/* 以只写方式打开目标文件,若此文件不存在则创建, 访问权限值为644,即user:读写、group:读、others:读权限,三位八进制数是664,返回的文件描述符赋值给dest_file*/
/*这里文件名字是宏定义*/
dest_file = open(DEST_FILE_NAME, O_WRONLY|O_CREAT, S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH);
/*判断文件描述符是什么,如果open函数返回-1表示打开失败*/
if (src_file < 0 || dest_file < 0)
{
printf("Open file error\n");
exit(1);
}
/* 将源文件的读写指针移到最后10KB的起始位置*/
/*SEEK_END将起点设置为文件的结尾,向前偏移10K字节,定为当前文件读写位置*/
/*如果是SEEK_SET起点就是文件开头,SEEK_CUR起点就是当前指针位置*/
lseek(src_file, -OFFSET, SEEK_END);
/* 读取源文件src_file的数据,放在数组buff中,读取的量是sizeof(buff)字节即1024字节*/
/*最后10KB数据并写到目标文件中,每次读写1KB */
/*当read正常返回独到的字节数的时候*/
while ((real_read_len = read(src_file, buff, sizeof(buff))) > 0)
{
/*写入dest_file文件,从buff字符数组,写real_read_len个字节数据*/
write(dest_file, buff, real_read_len);
}
/*最后关闭两个文件*/
close(dest_file);
close(src_file);
return 0;
}
文件锁
如果文件已经共享,如何让多个用户共同操作一个文件:给文件上锁,避免共享的资源产生竞争状态。 fcntl()函数就是实现的方法
fcntl()函数:不仅可以施加建议锁,还能上强制锁,还能对文件某一记录上记录锁。
记录锁分为读取锁(共享锁)和写入锁(排斥锁)
第一个参数 | 文件描述符 | ||||||
第二个参数 |
| ||||||
第三个参数 | lock:记录锁的具体状态 |
F_RDLCK | 读取锁 |
F_WRLCK | 写入锁 |
F_UNLCK | 解锁 |
记录锁源码:
/* lock_set.c 该函数作用:给fd文件描述符所对应的文件上type类型的锁(包括:读锁、写锁或解锁) */
/*测试代码中通过访问lock_set方法来上锁解锁*/
int lock_set(int fd, int type)
{
struct flock old_lock, lock;/*创建lock结构图*/
lock.l_whence = SEEK_SET;/* 文件读写指针起始位置在文件开头 */
lock.l_start = 0; /*相对位移量;加锁整个文件*/
lock.l_len = 0; /*加锁区域长度;加锁整个文件*/
lock.l_type = type; /*type包含:读锁、写锁和解锁三个类型;给唯一需要赋值的锁类型l_type赋值文件锁,这里是关键*/
lock.l_pid = -1; /*该文件拥有文件锁的进程号,默认为-1*/
/* 首先需要执行第一次fcntl()函数判断文件是否可以上锁,因为只有文件为解锁(不上锁)状态才能上锁 */
/*F_GETLK:根据lock参数值,决定是否上锁,get lock*/
/*&lock:取flock结构体变量lock的首地址*/
fcntl(fd, F_GETLK, &lock);
/*此处的fcntl()函数只是为了测试fd是否能上锁,因为只有文件为解锁(不上锁)状态才能上锁;如果文件已经上锁,
根据F_GETLK参数就要看情况判断:
如果之前已经存在的是读锁则还可以上读锁,如果是写锁则不能上读锁或写锁;
也就是说:要给一个没上锁的文件上锁要执行两次fcntl()函数;
第一次执行:F_GETLK参数将文件设置为解锁状态;fcntl()函数返回-1,表达的含义经过测试该文件可以上锁;
第二次fcntl()函数执行就可以放心大胆的上锁,fcntl()函数会返回0,上锁成功*/
/*如果lock.l_type不是解锁状态,意味着已经存在其他锁*/
if (lock.l_type != F_UNLCK)
{
/* 判断文件不能上锁的原因 */
if (lock.l_type == F_RDLCK) /* 该文件已有读锁 */
{
printf("Read lock already set by process PID:%d\n", lock.l_pid);
}
else if (lock.l_type == F_WRLCK) /* 该文件已有写锁 */
{
printf("Write lock already set by process PID:%d\n", lock.l_pid);
}
}
/* l_type 可能已被F_GETLK修改过,所以需要重新赋值*/
lock.l_type = type;
/* 根据不同的type值,第二次执行fcntl函数,进行阻塞式上锁或解锁,所谓阻塞式:意味着如果没有成功上锁或解锁或捕捉到信号,则程序一直处于阻塞状态 */
/*F_SETLKW:在无法获取锁时,进入睡眠,set lock wait;如果可获得锁或捕捉到信号,则返回*/
/*&lock:取flock结构体变量lock的首地址*/
if ((fcntl(fd, F_SETLKW, &lock)) < 0)
/*此处fcntl()函数作用:给fd文件描述符所对应的文件上lock结构体变量所确定的l_type类型的锁;
此时用F_GETLKW直接给文件上文件锁,因为第一次fcntl()函数已经测试过
该文件可以上锁,于是本次fcntl()函数可以成功执行,并返回0 */
{
printf("Lock failed:type = %d\n", lock.l_type);
return 1;
}
switch(lock.l_type)
{
case F_RDLCK:
{
printf("Read lock set by process PID:%d\n", getpid());/*该文件已被getpid()进程上了读锁*/
}
break;
case F_WRLCK:
{
printf("Write lock set by process PID:%d\n", getpid());/*该文件已被getpid()进程上了写锁*/
}
break;
case F_UNLCK:
{
printf("Release lock by process PID:%d\n", getpid());/*该文件已被getpid()进程解锁*/
return 1;
}
break;
default:
break;
}
return 0;
}
读取锁测试:
/* read_lock.c */
#include <unistd.h>
#include <sys/file.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <stdlib.h>
#include "lock_set.c"
int main(void)
{
int fd;/*整形变量fd存放文件标识符*/
/*读写方式打开,不存在就创建,权限0644*/
fd = open("hello",O_RDWR | O_CREAT, 0644);
if(fd < 0)
{
printf("Open file error\n");
exit(1);
}
lock_set(fd, F_RDLCK);/* 给文件上读取锁,上锁后程序处于等待键盘输入字符的等待状态; */
getchar();/*取键盘键入的字符,可以让程序继续执行下去*/
lock_set(fd, F_UNLCK);/* 给文件解锁 */
getchar();
close(fd);
exit(0);
}
写入锁测试:
/* write_lock.c */
#include <unistd.h>
#include <sys/file.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <stdlib.h>
#include "lock_set.c"
int main(void)
{
int fd;
fd = open("hello",O_RDWR | O_CREAT, 0644);/* 读写打开文件 */
if(fd < 0)
{
printf("Open file error\n");
exit(1);
}
lock_set(fd, F_WRLCK);/* 给文件上写入锁 */
getchar();
lock_set(fd, F_UNLCK);/* 给文件解锁 */
getchar();
close(fd);
exit(0);
}
多路复用
处理I/O操作可能会被阻塞的情况
select()函数有什么用:
- 提高性能:select能够帮助程序在处理多个输入/输出源时更加高效。通过使用select,可以使得程序在等待一个I/O操作完成时继续执行其他任务,从而提高整体性能。
- 可扩展性:select使得程序可以处理更多的并发连接。这对于开发服务器应用程序尤为重要,因为它们需要同时处理多个客户端连接。
- 跨平台兼容性:select是一个通用的I/O复用技术,它在不同的操作系统和平台上都有实现。这意味着使用select编写的代码具有较好的可移植性。
- 建立基础知识:掌握select这类基本的I/O复用技术,有助于理解更高级和更复杂的技术,例如poll和epoll等。
摘取自:https://blog.csdn.net/qq_21438461/article/details/130143669
以下源码演示一个调用select(函数)监听三个终端(两个管道文件虚拟终端,一个主程序虚拟终端)的输入,并且进行相应的处理。在主程序的虚拟终端中输入q/Q实现程序结束,在管道终端数据输入在主程序终端中显示。
/* multiplex_select.c */
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <time.h>
#include <errno.h>
/*宏定义*/
#define MAX_BUFFER_SIZE 1024 /* 缓冲区大小*/
#define IN_FILES 3 /* 多路复用时输入文件数目*/
#define TIME_DELAY 600 /* 超时时间秒数 */
#define MAX(a, b) ((a > b)?(a):(b))
int main(void)
{
int fds[IN_FILES];/*fds[0],fds[1],fds[2]*/
char buf[MAX_BUFFER_SIZE];
int i, res, real_read, maxfd;
struct timeval tv;
fd_set inset,tmp_inset;/*文件描述符集fd_set,用来保存由select函数监控的文件所对应的文件描述符*/
fds[0] = 0;
/*首先按一定的权限打开两个管道源文件*/
/*按照只读|非阻塞方式打开管道文件in1,非阻塞就是不能等待立即返回结果的含义*/
if((fds[1] = open ("in1", O_RDONLY|O_NONBLOCK)) < 0)
{
printf("Open in1 error\n");
return 1;
}
if((fds[2] = open ("in2", O_RDONLY|O_NONBLOCK)) < 0)
{
printf("Open in2 error\n");
return 1;
}
/*取出两个文件描述符中的较大者*/
maxfd = MAX(MAX(fds[0], fds[1]), fds[2]);
/*初始化文件描述符集inset,并在该集合中加入相应的三个文件描述符:标准输入,in1和in2的文件描述符*/
FD_ZERO(&inset);
for (i = 0; i < IN_FILES; i++)
{
FD_SET(fds[i], &inset);
}
/*fd_set是一组文件描述字(fd)的集合,它用一位来表示一个fd*/
/*过去,一个fd_set通常只能包含小于32的fd(文件描述字),因为fd_set其实只用了一个32位矢量来表示fd;现在,UNIX系统通常会在头文件<sys/select.h>中定义常量FD_SETSIZE,它是数据类型fd_set的描述字数量,其值通常是1024,*/
/*linux shell 输入如下命令就知道,其中的“-n: file descriptors”就是linux系统允许的文件描述符的最大限制值。
# ulimit -a
-f: file size (blocks) unlimited
-t: cpu time (seconds) unlimited
-d: data seg size (kb) unlimited
-s: stack size (kb) 8192
-c: core file size (blocks) 0
-m: resident set size (kb) unlimited
-l: locked memory (kb) 64
-p: processes 128
-n: file descriptors 1024*/
/*FD_SET(0, &set); 将set的0号位置1,如set原来是00000000,则现在变为10000000(对应10进制就是1,2进制转10进制从左往右算),这样fd==0文件描述字就被加进set */
/*FD_CLR(4, &set); 将set的4号位置0,如set原来是10001000,则现在变为10000000,这样fd==4的文件描述字就被从set中清除了 */
/*FD_ISSET(5, &set); 测试set的5号位是否为1,如果set原来是10000100(对应10进制就是33),则返回非零,表明fd==5的文件描述字在set中;否则返回0*/
FD_SET(0, &inset);
tv.tv_sec = TIME_DELAY;
tv.tv_usec = 0;
/*循环测试该文件描述符是否准备就绪,并调用select函数对相关文件描述符做对应操作*/
while(FD_ISSET(fds[0],&inset) || FD_ISSET(fds[1],&inset) || FD_ISSET(fds[2], &inset))
{
/*文件描述符集合的备份,避免每次进行初始化*/
tmp_inset = inset;
res = select(maxfd + 1, &tmp_inset, NULL, NULL, &tv);
/*测试写文件描述符集;
res = select(maxfd + 1, NULL,&tmp_inset , NULL, &tv);*/
/*maxfd+1:需要监视的文件描述符最大值+1,减少监控二进制位数,提高效率*/
/* &tmp_inset:由select()监视的读文件描述符集合*/
/*NULL:由select()监视的写文件描述符集合*/
/*NULL:由select()监视的异常出错文件描述符集合*/
/*&tv:若等待了tv时间还没有检测到任何文件描述符准备好,则立即返回*/
/*select()执行成功则返回准备好的文件描述符的数目,如果返回-1代表出错
该函数执行过后只有数据输入的文件描述符才能继续存在tmp_inset文件描述符集当中,没有输入的则清除出tmp_inset*/
switch(res)
{
case -1:
{
printf("Select error\n");
return 1;
}
break;
case 0: /* Timeout */
{
printf("Time out\n");
return 1;
}
break;
default:/*if res非0、非-1,即存在已经准备好的文件描述符,也就是说有某个管道文件或标准输入文件有数据输入*/
{
for (i = 0; i < IN_FILES; i++)
{
if (FD_ISSET(fds[i], &tmp_inset))
{
/*对buf字符数组的MAX_BUFER_SIZE(1024)个字符赋初值为0*/
memset(buf, 0, MAX_BUFFER_SIZE);
/*从fds[i]指向的文件中读1024个字符到buff字符数组*/
real_read = read(fds[i], buf, MAX_BUFFER_SIZE);
if (real_read < 0)
{
if (errno != EAGAIN)
/*在Linux环境下开发经常会碰到很多错误(设置errno),其中EAGAIN是其中比较常见的一个错误(比如用在非阻塞操作中)。从字面上来看,是提示再试一次。这个错误经常出现在当应用程序进行一些非阻塞(non-blocking)操作(对文件或socket)的时候。例如,以 O_NONBLOCK的标志打开文件/socket/FIFO,如果你连续做read操作而没有数据可读。此时程序不会阻塞起来等待数据准备就绪返回,read函数会返回一个错误EAGAIN,提示你的应用程序现在没有数据可读请稍后再试。*/
{
return 1;
}
}
else if (!real_read)/*real_read等于0:已读到文件末尾*/
{
close(fds[i]); /*关闭文件描述符*/
FD_CLR(fds[i], &inset);/*将该文件描述符从文件描述符集inset清除出去*/
}
else /*从文件描述符fds[i]中读到有效数据*/
{
if (i == 0)/*如果是标准输入文件(键盘)输入的数据,看是否是退出标记q或Q*/
{
if ((buf[0] == 'q') || (buf[0] == 'Q'))/*如果主程序收到q或Q字符,则程序终止*/
{
return 1;/*主程序退出*/
}
else{printf("if you want to quit,please type q!\n"}
}
else
/*如果不是标准输入文件输入的数据,那只能是管道文件输入的数据,将cat从管道文件输入的字符串打印在屏幕上*/
{
buf[real_read] = '\0';/*在字符数组最后一位添加\0,使之成为字符串*/
printf("%s", buf); /*按照字符串格式打印输出字符数组buf*/
}
}
} /* end of if */
} /* end of for */
}
break;
} /* end of switch */
} /*end of while */
exit(0);
}
标准I/O变成
用到的函数带缓冲,用法类似