1.前置基础
默认的代码模板可以先locate snippet.c,找到位置后可以改这个文件然后得以修改代码模板。
可以把所有用的头文件放到一个文件中,然后放在/usr/include/下
iso c标准
打开文件:
FILE *fopen(const char *pathname,const char *mode);
"a"和"a+"模式,后者写时文件指针自动偏移到文件末尾
“r”和“r+”模式,“w”和“w+”模式
获取当前位置:
long ftell(FILE *stream);
成功返回当前位置,失败返回-1
移动指针:
int fseek(FILE *stream,long offset,int whence);
whence参数:SEEK_SET,SEEK_CUR,SEEK_END
读写操作:
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
size_t fwrite(const void *ptr, size_t size, size_t nmemb,FILE *stream);
int fputc(int c, FILE *stream);
int fputs(const char *s, FILE *stream);
int fgetc(FILE *stream);
char *fgets(char *s, int size, FILE *stream);
2.一些文件相关的系统调用
修改权限:int chmod(const char *pathname,mode_t mode);
获取当前工作目录:char *getcwd(char *buf,size_t size);
若参数为:NULL 0,则返回值内存是函数所分配
改变当前工作目录:int chdir(const char *path);
改变当前进程的工作目录,并不是shell,而是运行程序而启动的进程。
创建目录:int mkdir(const char *pathname,mode_t mode);
权限受umask的影响
删除目录:int rmdir(const char *pathname);
访问目录(目录流):
-
DIR *opendir(const char *name);
-
int closedir(DIR *dirp);
-
struct dirent *readdir(DIR *dirp);其中struct dirent中包括索引结点号,next指针,结构体长度,文件类型,文件名字,每次readdir后,取出ptr所指目录项数据,ptr后移。
可用opendir和readdir配合遍历一个目录内的目录项
-
调整ptr的位置:
long telldir(DIR *dirp);
void seekdir(DIR *dirp,long loc);
-
回到最开始:void rewinddir(DIR *dirp);
//遍历目录举例 int main(int argc, char *argv[]) { ARGS_CHECK(argc,2); DIR *dirp = opendir(argv[1]); ERROR_CHECK(dirp,NULL,"opendir"); struct dirent *pdirent; long loc; while((pdirent = readdir(dirp)) != NULL){ printf("ino = %ld, reclen = %d, type = %d, name = %s\n", pdirent->d_ino, pdirent->d_reclen, pdirent->d_type, pdirent->d_name); if(strcmp(pdirent->d_name,"ftell.c") == 0){ loc = telldir(dirp); } } puts("\n"); seekdir(dirp,loc); pdirent = readdir(dirp); printf("ino = %ld, reclen = %d, type = %d, name = %s\n", pdirent->d_ino, pdirent->d_reclen, pdirent->d_type, pdirent->d_name); closedir(dirp); return 0; }
stat文件信息:int stat(const char *pathname,struct stat *statbuf);
从用户id到用户名:struct passwd *getpwuid(uid_t uid);
从组id到组名:struct group *getgrgid(gid_t gid);
日历时间显示:
char *ctime(const time_t *timep); 返回一个固定格式的日历字符串。
struct tm *localtime(const time_t *timep);
//实现类ls命令
int main(int argc, char *argv[])
{
ARGS_CHECK(argc,2);
DIR *dirp = opendir(argv[1]);
ERROR_CHECK(dirp,NULL,"opendir");
struct dirent *pdirent;
while((pdirent = readdir(dirp)) != NULL){
struct stat statbuf;
char buf[512] = {0};
sprintf(buf,"%s%s%s",argv[1],"/",pdirent->d_name);
int ret = stat(buf,&statbuf);
ERROR_CHECK(ret,-1,"stat");
//类型 权限 硬连接数 用户名 组名 大小 修改时间 文件名字
printf("%8o %ld %s %s %10ld %15s %s",
statbuf.st_mode,
statbuf.st_nlink,
getpwuid(statbuf.st_uid)->pw_name,
getgrgid(statbuf.st_gid)->gr_name,
statbuf.st_size,
pdirent->d_name,
ctime(&statbuf.st_mtime)
);
}
return 0;
}
//tree命令实现
#include <func.h>
int DFSprint(char *path,int width);
int main(int argc, char *argv[])
{
// ./myTree dir
ARGS_CHECK(argc,2);
puts(argv[1]);
DFSprint(argv[1],4);
return 0;
}
int DFSprint(char *path,int width){
DIR* dirp = opendir(path);
char newPath[512] = {0};
ERROR_CHECK(dirp,NULL,"opendir");
struct dirent *pdirent;
while((pdirent = readdir(dirp)) != NULL){
if(strcmp(pdirent->d_name,".") == 0 ||strcmp(pdirent->d_name,"..") == 0){
continue;
}
printf("%*s%s\n",width,"",pdirent->d_name);
if(pdirent->d_type == 4){
sprintf(newPath,"%s%s%s",path,"/",pdirent->d_name);
DFSprint(newPath,width+4);
}
}
closedir(dirp);
return 0;
}
3.不带缓冲区的文件IO
是不带用户态缓冲区的IO
int open(const char *pathname,int flags);
flags:O_RDONLY|O_WRONLY|O_RDWR|O_CREAT|O_TRUNC|O_APPEND|O_EXCL(如果文件存在就报错)等等
int open(const char *pathname,int flags,mode_t mode);
成功返回文件描述符fd(指针数组的下标,该数组中内容所对应的地址是打开文件表的对应项/文件对象/内核文件缓冲区的地址),失败返回-1
ssize_t read(int fd,void *buf,size_t count);
ssize_t write(int fd,const void *buf,size_t count);
:%!xxd 在vim中将文件转化为16进制,加-r返回原来格式
//cp命令实现
#include <func.h>
int main(int argc, char *argv[])
{
// ./myCp srcs dest
ARGS_CHECK(argc,3);
int fdr = open(argv[1],O_RDONLY);
ERROR_CHECK(fdr,-1,"open fdr");
int fdw = open(argv[2],O_WRONLY|O_CREAT|O_TRUNC,0666);
ERROR_CHECK(fdw,-1,"open fdw");
char buf[4096] = {0};
while(1){
memset(buf,0,sizeof(buf));
int ret = read(fdr,buf,sizeof(buf));
if(ret == 0){
break;
}
write(fdw,buf,ret);
}
close(fdr);
close(fdw);
return 0;
}
fopen VS open: fopen多了一次拷贝,先拷贝到用户态文件对象,再拷贝一次到内核态缓冲区,open直接拷贝到内核态缓冲区。当拷贝的内容大于4096时,open效率高,当小于4096时,fopen效率高,因为陷入内核态的次数小。
4.一些其他的文件操作
int ftruncate(int fd,off_t length);
使文件长度为指定长度:可实现文件空洞,成功返回0,失败-1
#include <func.h>
int main(int argc, char *argv[])
{
ARGS_CHECK(argc,2);
int fd = open(argv[1], O_RDWR);
ERROR_CHECK(fd,-1,"open");
int ret = ftruncate(fd,40960);
ERROR_CHECK(ret,-1,"ftruncate");
close(fd);
return 0;
}
内核态文件到用户态缓冲区的映射(内核态文件与磁盘通过DMA方式来进行自动信息传递),使操作内核态文件跟操作数组一样:
void *mmap(void *addr,size_t length,int prot,int flags,int fd,off_t offset);
prot参数:建立的映射的权限:PROT_EXEC(执行),PROT_READ,PROT_WRITE
flags参数:映射是否对其他进程可见:MAP_SHARED,MAP_PRIVATE
int munmap(void *addr,size_t length);
#include <func.h>
int main(int argc, char *argv[])
{
ARGS_CHECK(argc,2);
int fd = open(argv[1], O_RDWR);
ERROR_CHECK(fd,-1,"open");
int ret = ftruncate(fd,5);
ERROR_CHECK(ret,-1,"ftruncate");
char *p = (char *)mmap(NULL,5,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);
ERROR_CHECK(p,MAP_FAILED,"mmap");
//p[0] ~ p[4]是属于映射区的
p[5] = '\0';
puts(p);//读文件
p[0] = 'H';//写文件
munmap(p,5);
close(fd);
return 0;
}
off_t lseek(int fd,off_t offset,int whence);使文件读写偏移,可实现文件空洞
成功返回偏移量,失败返回-1
#include <func.h>
int main(int argc, char *argv[])
{
ARGS_CHECK(argc,2);
int fd = open(argv[1], O_RDWR);
ERROR_CHECK(fd,-1,"open");
lseek(fd,40960,SEEK_SET);
write(fd,"1",1);
close(fd);
return 0;
}
文件流底层用了文件描述符:int fileno(FILE *stream);实现文件流到文件描述符
重定向:
int dup(int oldfd);选择一个最小可用的fd,指向oldfd所指的fo(共享文件指针);
int dup2(int oldfd,int newfd);把oldfd所指的文件对象,复制到newfd所指。
#include <func.h>
int main(int argc, char *argv[])
{
ARGS_CHECK(argc,2);
int fd1 = open(argv[1],O_RDWR);
ERROR_CHECK(fd1,-1,"open");
printf("fd1 = %d\n", fd1);
int fd2 = 10;
dup2(STDOUT_FILENO,fd2);
dup2(fd1,STDOUT_FILENO);
printf("You can't see me!\n");
dup2(fd2,STDOUT_FILENO);
printf("You can see me!\n");
close(fd1);
close(fd2);
return 0;
}
5.有名管道
有名管道(进程间的通信方式)
创建有名管道的命令:mkfifo 管道名(*.pipe)
使用系统调用访问管道:
可用open打开管道(参数flag按照读写选择)
读端:O_RDONLY
写端:O_WRONLY
使用write、read来进行读写;
//简陋的及时聊天
//chat1.c
#include <func.h>
int main(int argc, char *argv[])
{
// ./chat1 1.pipe 2.pipe
ARGS_CHECK(argc,3);
int fdr = open(argv[1], O_RDONLY);
int fdw = open(argv[2], O_WRONLY);
puts("chat1");
char buf[4096] = {0};
while(1){
memset(buf,0,sizeof(buf));
read(STDIN_FILENO,buf,sizeof(buf));
write(fdw,buf,strlen(buf));
memset(buf,0,sizeof(buf));
read(fdr,buf,sizeof(buf));
puts(buf);
}
return 0;
}
//chat2.c
#include <func.h>
int main(int argc, char *argv[])
{
// ./chat2 1.pipe 2.pipe
ARGS_CHECK(argc,3);
int fdw = open(argv[1], O_WRONLY);
int fdr = open(argv[2], O_RDONLY);
puts("chat2");
char buf[4096] = {0};
while(1){
memset(buf,0,sizeof(buf));
read(fdr,buf,sizeof(buf));
puts(buf);
memset(buf,0,sizeof(buf));
read(STDIN_FILENO,buf,sizeof(buf));
write(fdw,buf,strlen(buf));
}
return 0;
}
6.I/O多路复用
1.创建集合,集合元素时fd,把要等待的文件描述符放到集合中监听
fd_set set; //创建集合,将要等待的fd放入集合。
void FD_ZERO(fd_set *set); //集合初始化
void FD_SET(int fd,fd_set *set); //将要等待监听的文件描述符放到集合中
2.用select系统调用,进程阻塞,当集合中有任一个fd就绪,解除阻塞。
int select(int nfds,fd_set *readfds,fd_set *writefds,fd_set *exceptfds,struct timeval *timeout);
nfds传监听的最大文件描述符数字+1,readfds读集合,writefds写集合
exceptfds额外信息集合,timeout时间结构体,超时返回0
struct timeval{ long tv_sec; long tv_nsec; };
3.用FD_ISSET()检查是否就绪,不就绪跳过,就绪获取资源。
int FD_ISSET(int fd,fd_set *set);
即时聊天写端关闭:
1.写端关闭,读端会一直读到文件终止符,造成死循环。
2.读端关闭,写端继续写,会触发一个SIGPIPE信号,导致程序崩溃。
//aqiang.c
#include <func.h>
int main(int argc, char *argv[])
{
// ./chat2 1.pipe 2.pipe
ARGS_CHECK(argc,3);
int fdw = open(argv[1], O_WRONLY);
int fdr = open(argv[2], O_RDONLY);
puts("chat2");
char buf[4096] = {0};
fd_set rdset;
while(1){
FD_ZERO(&rdset);
FD_SET(fdr,&rdset);
FD_SET(STDIN_FILENO, &rdset);
struct timeval timeout;
timeout.tv_sec = 2;
timeout.tv_usec = 500000;
int nret = select(fdr+1,&rdset,NULL,NULL,&timeout);
if(nret == 0){
puts("timeout");
}
if(FD_ISSET(fdr,&rdset)){
puts("msg from pipe");
memset(buf,0,sizeof(buf));
int ret = read(fdr,buf,sizeof(buf));
if(ret == 0){
puts("byebye");
break;
}
puts(buf);
}
if(FD_ISSET(STDIN_FILENO, &rdset)){
puts("msg from stdin");
memset(buf,0,sizeof(buf));
int ret = read(STDIN_FILENO,buf,sizeof(buf));
if(ret == 0){
puts("I quit");
write(fdw,"nishigehaoren",13);
break;
}
write(fdw,buf,strlen(buf));
}
}
return 0;
}
//azhen.c
#include <func.h>
int main(int argc, char *argv[])
{
// ./chat1 1.pipe 2.pipe
ARGS_CHECK(argc,3);
int fdr = open(argv[1], O_RDONLY);
int fdw = open(argv[2], O_WRONLY);
puts("chat1");
char buf[4096] = {0};
fd_set rdset;
while(1){
FD_ZERO(&rdset);
FD_SET(fdr,&rdset);
FD_SET(STDIN_FILENO, &rdset);
select(fdr+1,&rdset,NULL,NULL,NULL);
if(FD_ISSET(fdr,&rdset)){
puts("msg from pipe");
memset(buf,0,sizeof(buf));
int ret = read(fdr,buf,sizeof(buf));
if(ret == 0){
puts("byebye");
break;
}
puts(buf);
}
if(FD_ISSET(STDIN_FILENO, &rdset)){
puts("msg from stdin");
memset(buf,0,sizeof(buf));
int ret = read(STDIN_FILENO,buf,sizeof(buf));
if(ret == 0){
puts("I quit");
write(fdw,"nishigehaoren",13);
break;
}
write(fdw,buf,strlen(buf));
}
}
return 0;
}
select原理:fd_set是个位图
1.把fd_set拷贝到内核中
2.遍历位图中从0 - ndfs-1
3.如果某个设备文件就绪,就不改变对应fd_set中的位,如果未就绪,就置为0,故每次循环结束都要把要监听的文件描述符重新加入集合
问题:效率低,O(N),同时打开设备有上限,1024
用epoll,红黑树,文件无上限
超时机制参数:struct timeval *timeout;
写阻塞问题:
1.读得慢写的快
2.写的时候要写的数据大于管道最大容量(4096),会阻塞在写的时候,比如要写4097个字节的数据,前4096个写满管道了,然后写最后一个一直写不进去,然后就阻塞在这了。(可以通过ulimit -a来查看管道最大容量)
3.一个文件同时读写。
#include <func.h>
int main(int argc, char *argv[])
{
ARGS_CHECK(argc,2);
int fdr = open(argv[1],O_RDWR);//非阻塞方式建立管道读端
int fdw = open(argv[1],O_RDWR);
puts("established");
fd_set rdset;
fd_set wrset;
char buf[4096] = {0};
int cnt1 = 0;
int cnt2 = 0;
while(1){
FD_ZERO(&rdset);
FD_ZERO(&wrset);
FD_SET(fdr,&rdset);
FD_SET(fdw,&wrset);
select(fdw+1,&rdset,&wrset,NULL,NULL);
if(FD_ISSET(fdr,&rdset)){
printf("I can read cnt = %d\n",cnt1++);
read(fdr,buf,2048);
}
//第一次进入循环无可读先写
if(FD_ISSET(fdw,&wrset)){
printf("I can write cnt = %d\n",cnt2++);
write(fdw,buf,4096);
}
}
return 0;
}