当程序执行标准库函数printf时,会用到文件指针FILE *stdout。
stdout主要由三部分构成,如下图。
stdout实际就对应文件描述符中的1。
执行标准库函数printf时,程序会调用内核提供的系统调用函数write。
我们针对一个文件的操作可以规定为这5个操作:open、close、read、write、lseek
open、close、read、write、lseek
open
因为他们都属于系统调用,所以在查看时要加上2,表示查看第二章的。
比如查看第二章open的使用方法,man 2 open
参数:
pathname:文件名
flags,
必选项
O_RDONLY, O_WRONLY, or O_RDWR.
可选项
O_APPEND 追加
O_CREAT (文件不存在时)创建
O_EXCL必须和O_CREAT一起使用,(如果文件存在则报错。)
mode:权限位
mode指定创建新文件时使用的权限。
当O_CREAT或O_TMPFILE在flags中指定时必须提供此参数;如果没有指定O_CREAT和O_TMPFILE,则忽略mode。
最终(mode & ~umask)
O_NONBLOCK 非阻塞
必选项和可选项实际上是一个32位的位图
返回值
成功返回当前最小的可用的文件描述符。失败返回-1.
默认一个进程启动之后,0、1、2是被使用的,对应标准输入、标准输出和标准错误,此时再打开一个文件,将返回的文件描述符是3。
如果把0,1,2的其中一个或多个给关闭了,则返回被关的最小的那个文件描述符。
close
同样查看open的使用方法,man 2 close
参数:open打开的文件描述符。
返回值:成功返回0,失败返回-1.
使用open和close函数实现touch命令的部分功能(打开或新建一个文件,并指定权限。)
//printf需要用
#include <stdio.h>
//open需要用
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
//close需要用
#include <unistd.h>
int main(int argc,char* argv[])
{
if(argc!=2)
{
printf("%s filename.\n",argv[0]);
return -1;
}
int fd = open(argv[1],O_RDONLY|O_CREAT,0666);
//0666&(~umask) = 0666&(~0002)=0666&7775=0665,rw- rw- r-x,因为是文件,所以x也会被拿走,最后是rw- rw- r--
close(fd);
return 0;
}
运行代码:
只读打开一个名为creat_test的文件,如果文件不存在就创建它,指定权限为rw- rw- r–
read
从fd中最多读count字节。
read是阻塞等待的
write
实现一个cat功能
#include<stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main(int argc,char* argv[])
{
if(argc!=2)
{
printf("%s filename\n",argv[0]);
return -1;
}
int fd = open(argv[1],O_RDONLY);
//读,输出到屏幕
char buf[256];
int ret=0;
do
{
//从fd中读,读到buf中,最多读sizeof(buf)个字节。
//fd会递增的,最终指向文件末尾
ret=read(fd,buf,sizeof(buf));
//测试是否不管文件中还剩多少字节,每次都读count
printf("%d\n",ret);//测出的结果是有多少读多少,最多读到count.
//STDOUT_FILENO是标准输出的宏定义,值为1
write(STDOUT_FILENO,buf,ret);
}while(ret>0&&ret==sizeof(buf));//代表读到末尾了
close(fd);
return 0;
}
lseek实现文件读写位置改变
需求:打开一个文件,写入内容:helloworld,然后读取下该文件的内容,输出到屏幕。
先来实现下。
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main(int argc,char* argv[])
{
if(argc!=2)
{
printf("%s filename\n",argv[0]);
return -1;
}
char str[]="hello world";
int fd = open(argv[1],O_RDWR|O_CREAT,0666);
//STDOUT_FILENO是标准输出的宏定义,值为1
write(fd,str,sizeof(str));
//读,输出到屏幕
char buf[256];
memset(buf,0,sizeof(buf));
int ret=0;
do
{
ret=read(fd,buf,sizeof(buf));
printf("%d\n",ret);
//STDOUT_FILENO是标准输出的宏定义,值为1
write(STDOUT_FILENO,buf,ret);
}while(ret>0&&ret==sizeof(buf));
close(fd);
return 0;
}
运行一下:
原因出在哪呢?为什么没有输出出来呢?
有没有写到my_lseek.log这个文件中呢?
打开my_lseek.log这个文件看下。
写进去了。
那为什么呢?
是因为在write之后,读写文件指针 fd 移动到文件末尾了,如果我们要想输出出内容,就必须将读写文件指针 fd 移动到开头,这需要用到一个函数lseek。
看下lseek这个函数。
函数的作用是:根据下面的指令,将与文件描述符fd关联的打开文件的偏移量重新定位到参数offset上:
返回值
成功:返回当前位置到开始位置的偏移的字节数。
失败:返回 -1。
在上述代码中添加:
lseek(fd,0,SEEK_SET);
lseek实现文件读写位置改变
验证Linux能够打开文件的最大数量
Linux能够打开的最大文件数量为1024(文件描述符为0-1023)个。
默认一个进程启动之后,0、1、2是被使用的,对应标准输入、标准输出和标准错误,此时再打开一个文件,将返回的文件描述符是3。
因为这里的主要目的是验证能打开多少文件,所以就不关闭文件描述符,正常来说是要打开一个文件关闭一个文件的。
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
//验证Linux能打开的最大文件数量
int main()
{
//默认再打开的文件是从3开始的
//前面三个已打开的文件分别是标准输入、标准输出、标准错误
int num = 3;
int count = 0;
char filename[128];
while(1)
{
sprintf(filename,"temp_%04d",num++);
if(open(filename,O_RDONLY|O_CREAT,0666) < 0 )
{
perror("open error");
break;
}
else
{
//open sucess
count += 1;
}
}
printf("count = %04d.\n",count);
return 0;
}
程序运行输出结果如下:
stat函数
获得文件信息
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
int stat(const char *pathname, struct stat *buf);
//通过一个文件名获取文件属性
int fstat(int fd, struct stat *buf);
//通过一个已经打开的文件描述符来获取文件的属性
int lstat(const char *pathname, struct stat *buf);
//可以发现与stat的参数和返回值都相同,但这个函数前面加上了一个字母l,说明这个函数与stat在处理链接文件有不同的之处。
我们使用 ls -l 实现的也是查看文件的详细信息,因此使用这个函数可以实现相同的功能。
函数参数:
const char *pathname, //文件名
struct stat *buf //一个指向struct stat结构体的指针,是传出参数
返回值:
On success, zero is returned. On error, -1 is returned, and errno is set appropriately.
主要看下这个struct stat结构体。
struct stat {
dev_t st_dev; /* ID of device containing file */包含该文件的设备号ID
ino_t st_ino; /* inode number */文件的索引节点
mode_t st_mode; /* protection */ 文件类型和权限
nlink_t st_nlink; /* number of hard links */硬连接数量
uid_t st_uid; /* user ID of owner */用户的ID
gid_t st_gid; /* group ID of owner */组ID
dev_t st_rdev; /* device ID (if special file) */
off_t st_size; /* total size, in bytes */文件大小
blksize_t st_blksize; /* blocksize for filesystem I/O */块的大小
blkcnt_t st_blocks; /* number of 512B blocks allocated */块的个数
/* Since Linux 2.6, the kernel supports nanosecond
precision for the following timestamp fields.
For the details before Linux 2.6, see NOTES. */
struct timespec st_atim; /* time of last access */上一次的访问时间
struct timespec st_mtim; /* time of last modification */上一次的修改时间
struct timespec st_ctim; /* time of last status change *//上一次的状态变化的时间
#define st_atime st_atim.tv_sec /* Backward compatibility */
#define st_mtime st_mtim.tv_sec
#define st_ctime st_ctim.tv_sec
};
说一说这个结构体的一些成员变量
1、st_ino; // inode number
inode是index node,表示索引结点,里面有文件存储在磁盘的哪个位置的信息,以及文件大小、时间等等信息。
2、st_mode; // 文件类型和权限
判断文件类型可以根据掩码值。其中,第一位的0表示八进制。
也就是说,7代表二进制的111.
如果不想使用掩码这种处理方式,还可以使用下面的几种宏。
此外,还有几位数分别用来表示特殊的权限位和三类人的权限。
比如下图中的第一位表示特殊的权限位(很少使用)和另外三位数则表示三类人的权限。
同样都是八进制数。
3、再来说一说这个结构体成员变量中的数据类型 struct timespec
先来说一说怎么找这个结构体
假设一开始并不知道这个结构体在哪?只知道有这个结构体名。
那么就可以在根目录中使用grep -rn来搜索。
-r代表递归查找,-n代表显示行号。
此外,找结构体时,有技巧,就是在结构体类型名后加上{,还有就是一般都在/usr目录下。
grep -rn "struct timespec {" /usr/
查找比较慢,需要耐心等待。
应该是在
/usr/include/linux/time.h:9:struct timespec {
打开查找到的文件。
使用命令
其中的+9表示可以定位到改行。
struct timeval {
__kernel_time_t tv_sec; /* seconds */
__kernel_suseconds_t tv_usec; /* microseconds */
};
除了stat这个函数之外呢,还有stat这个命令。
比如使用stat这个命令来查看daemon.c这个文件的详细信息,就可以使用命令stat daemon.c
输出结果如下:
应用stat函数–实现ls命令
实现目标:
实现目标:
获取文件用户的相关信息 – getpwuid
需要传入uid_t uid
#include <sys/types.h>
#include <pwd.h>
struct passwd *getpwuid(uid_t uid);
返回值是struct passwd类型的值
The passwd structure is defined in <pwd.h> as follows:
struct passwd {
char *pw_name; /* username */
char *pw_passwd; /* user password */
uid_t pw_uid; /* user ID */
gid_t pw_gid; /* group ID */
char *pw_gecos; /* user information */
char *pw_dir; /* home directory */
char *pw_shell; /* shell program */
};
获取文件组的相关信息 – getgrgid
需要传入gid_t gid
#include <sys/types.h>
#include <grp.h>
struct group *getgrgid(gid_t gid);
返回值是struct group 类型的值
The group structure is defined in <grp.h> as follows:
struct group {
char *gr_name; /* group name */
char *gr_passwd; /* group password */
gid_t gr_gid; /* group ID */
char **gr_mem; /* NULL-terminated array of pointers
to names of group members */
};
时间获取 – localtime
需要传入const time_t * timep
#include <time.h>
struct tm *localtime(const time_t *timep);
返回值是struct tm 类型的值
Broken-down time is stored in the structure tm, which is defined in <time.h> as follows:
struct tm {
int tm_sec; /* Seconds (0-60) */
int tm_min; /* Minutes (0-59) */
int tm_hour; /* Hours (0-23) */
int tm_mday; /* Day of the month (1-31) */
int tm_mon; /* Month (0-11) */
int tm_year; /* Year - 1900 */
int tm_wday; /* Day of the week (0-6, Sunday = 0) */
int tm_yday; /* Day in the year (0-365, 1 Jan = 0) */
int tm_isdst; /* Daylight saving time */
};
具体的实现代码如下:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <time.h>
#include <string.h>
#include <pwd.h>
#include <grp.h>
//-rw-rw-r-- 1 book book 1353 8月 17 17:31 daemon.c
void file_type(struct stat m,char *buf)
{
if(S_ISREG(m.st_mode)) //is it a regular file?
buf[0]='-';
else if(S_ISDIR(m.st_mode)) // directory?
buf[0]='d';
else if(S_ISCHR(m.st_mode)) // character device?
buf[0]='c';
else if(S_ISBLK(m.st_mode)) //block device?
buf[0]='b';
else if(S_ISFIFO(m.st_mode)) //FIFO (named pipe)?
buf[0]='p';
else if(S_ISLNK(m.st_mode)) //symbolic link? (Not in POSIX.1-1996.)
buf[0]='l';
else if(S_ISSOCK(m.st_mode)) //symbolic link? (Not in POSIX.1-1996.)
buf[0]='s';
}
void file_permission(struct stat m,char* buf)
{
if(m.st_mode & S_IRUSR)
buf[1]='r';
if(m.st_mode & S_IWUSR)
buf[2]='w';
if(m.st_mode & S_IXUSR)
buf[3]='x';
if(m.st_mode & S_IRGRP)
buf[4]='r';
if(m.st_mode & S_IWGRP)
buf[5]='w';
if(m.st_mode & S_IXGRP)
buf[6]='x';
if(m.st_mode & S_IROTH)
buf[7]='r';
if(m.st_mode & S_IWOTH)
buf[8]='w';
if(m.st_mode & S_IXOTH)
buf[9]='x';
}
//时间获取
struct tm *get_FileAcessTime(const time_t *timep)
{
struct tm *get_FileTime = localtime(timep);
return get_FileTime;
}
int main(int argc,char* argv[])
{
if(argc!=2)
{
printf("./%s filename.\n",argv[0]);
return -1;
}
//调用stat 得到文件属性信息
struct stat sb;
stat(argv[1],&sb);
//获取文件类型信息和文件权限信息,并保存到数组中
char stmode[11];
memset(stmode,'-',sizeof(stmode));
//文件类型
file_type(sb,stmode);
//文件权限位
file_permission(sb,stmode);
stmode[10]='\0';
//硬连接数量--可通过struct stat来获得
//文件用户名--可通过函数getpwuid来获得
//文件组用户名--可通过函数getgrgid来获得
//文件大小--可直接通过struct stat来获得
//时间可通过函数localtime来获取
//获取文件的时间信息,并保存到数组中
char timebuf[20];
struct tm *get_Time = get_FileAcessTime(&sb.st_atim.tv_sec);
sprintf(timebuf,"%d月 %d %d:%d",get_Time->tm_mon+1,get_Time->tm_mday,get_Time->tm_hour,get_Time->tm_min);
//-rw-rw-r-- 1 book book 1353 8月 17 17:31 daemon.c
printf("%s %lu %s %s %lu %s %s\n",stmode,sb.st_nlink,getpwuid(sb.st_uid)->pw_name,getgrgid(sb.st_gid)->gr_name,sb.st_size,timebuf,argv[1]);
return 0;
}
程序运行结果并验证:
access
功能:判断当前用户对该文件的权限以及文件是否存在。
#include <unistd.h>
int access(const char *pathname, int mode);
mode:
R_OK(是否有读权限), W_OK(是否有写权限), and X_OK(是否有可执行权限). F_OK(文件是否存在)
返回值:
如果有对应权限或文件存在,则返回0.
失败返回-1,并设置errno.
#include <stdio.h>
#include <unistd.h>
int main(int argc,char* argv[])
{
if(argc!=2)
{
printf("%s filename.\n",argv[0]);
return -1;
}
if(access(argv[1],F_OK)==0)
{
printf("%s Find ok.\n",argv[1]);
if(access(argv[1],R_OK)==0)
{
printf("%s read ok.\n",argv[1]);
}
else
{
perror("read error");
}
if(access(argv[1],W_OK)==0)
{
printf("%s write ok.\n",argv[1]);
}
else
{
perror("write error");
}
if(access(argv[1],X_OK)==0)
{
printf("%s excute ok.\n",argv[1]);
}
else
{
perror("execute error");
}
}
else
{
perror("no find");
}
return 0;
}
程序运行结果如下:
文件所属为book用户,当前在book用户下,所以看用户对应的读写可行性权限。
也就是第一个rwx。
假设当前Linux系统上有用户itheima,还有其他用户yekai,且在yekai用户下有一个文件xxx.sh,当前用户为itheima,那么在当前用户下判断该文件的读写权限,执行结果将怎么样,又应该怎么看呢?
在当前用户下执行命令查看,
itheima对应该文件所属,属于其他用户,所以应该看第三个,为r-x.
我们再执行函数来判断下:
果然。
如果我们在前面加上sudo会怎么样呢?
使用sudo之后,当前用户变成root,对该文件来说,root用户拥有所有权限。
truncate
功能:截断文件。
使用man 2 truncate来查看函数说明。
#include <unistd.h>
#include <sys/types.h>
int truncate(const char *path, off_t length);
参数:
path:文件名,path对应的文件必须存在。
length:长度,长度如果大于源文件,直接拓展,如果小于源文件,截断为length长度。
返回值:
RETURN VALUE
On success, zero is returned. On error, -1 is returned, and errno is set appropriately.
假设当前目录下有普通文件hello,文件内容为:
文件大小为:
执行代码
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main()
{
int size=0;
scanf("%d",&size);
truncate("hello",size);
return 0;
}
执行代码,输入512之后。
打开文件看下文件内容。
再执行代码,输入10之后。
打开文件看下。
原子操作
原子操作:不可分割的操作。
原子:不可分割的最小单位。
原子操作的作用:解决竞争和冲突。
在多线程和多进程并发中,解决竞争和冲突,最首要的操作就是将某些操作给原子化,比如互斥量加锁。
dup和dup2
#include <unistd.h>
int dup(int oldfd);
int dup2(int oldfd, int newfd);
dup和dup2的作用不同。
dup是复制参数所指定的文件描述符,然后返回一个当前可用的最小文件描述符。
dup2是重定向。
1、dup2会将后面参数(newfd)的指向重定向到前面参数(oldfd)的指向。
2、如果oldfd是一个可用的文件描述符,并且newfd与oldfd的值相同,那么dup2将什么都不做,并且返回newfd
3、dup2操作原子
close(1);
dup(fd);
dup2(fd,1);//在功能上等价与上面两句,但dup2比上面两句原子(不会因为时间片的结束而被中途切换出去)
dup和dup2的练习
在代码中执行2次
printf();
第一次输出到hello文件中,
后一次输出到屏幕上。
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
//先备份现场
int outfd =dup(1);
int fd = open("hello",O_WRONLY|O_CREAT,0666);
dup2(fd,1);
printf("Hello.\n");
close(fd);
dup2(outfd,1);
printf("Hello.\n");
return 0;
}
运行程序:
再看下是否创建了hello这个文件
创建了,但是里面没有内容。
两次输出都输出到了屏幕终端。
那到底是什么原因呢?
是因为没有刷新。
下面结合多方面因素,完善这个程序,让其尽量符合多线程的Linux编程思想。
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
//现在探讨的这个程序考虑到的是当前实际、微观、宏观
int main()
{
//先备份现场
//返回当前可用的最小描述符,应该是3
int outfd =dup(1);
printf("outfd =%d.\n",outfd);//输出到终端上.
int fd = open("hello",O_WRONLY|O_CREAT,0666);
printf("fd =%d.\n",fd);//返回最小的文件描述符,应该是4
//执行dup2函数后,
//dup2函数的功能是先执行close(1),再将1重定向到fd所指向的文件。
dup2(fd,1);//将标准输出重定向到打开的这个文件,
printf("Hello.\n");
fflush(stdout);//刷新缓冲区
if(fd!=1)
{
//宏观上看:因为Linux是多线程并发,避免你把标准输出给关闭了,别人用不了了。
close(fd);
//需要刷新,如果是以fopen()打开fclose()关闭,
//应该就不用刷新了吧,没有经过验证,只是这样想.
}
dup2(outfd,1);//将标准输出重新定向到终端上。
printf("Hello.\n");
return 0;
}
程序运行输出结果如下:
fcntl
管家级的函数,与文件相关的操作归它管,可以用它来实现上面提到的诸如dup、dup2等相关函数的功能。
NAME
fcntl - manipulate file descriptor 操作文件描述符
SYNOPSIS
#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* arg */ );
DESCRIPTION
fcntl() performs one of the operations described below on the open file descriptor fd. The operation is determined by cmd.
cmd是指你要对这个文件描述符做什么事情,之后的参数的含义是你要对这个文件描述符做的事情是否需要传参。
由于命令不同,会造成给该函数传参不同,同时返回值也不同。
命令和返回值,具体可参考man手册。
比如文件状态标志File status flags
The file status flags and their semantics(语义) are described in open(2).
F_GETFL (void)
Get the file access mode and the file status flags; arg is ignored.
获取文件访问模式和文件状态标志,此时不需要传参。
F_SETFD (int)
Set the file status flags to the value specified by arg. File access mode (O_RDONLY, O_WRONLY, O_RDWR) and file creation flags (i.e., O_CREAT,O_EXCL, O_NOCTTY, O_TRUNC) in arg are ignored. On Linux this command can change only the O_APPEND, O_ASYNC, O_DIRECT, O_NOATIME, and O_NON‐BLOCK flags. It is not possible to change the O_DSYNC and O_SYNC flags; see BUGS, below.
将文件状态标志设置为arg指定的值。arg中的文件访问模式(O_RDONLY, O_WRONLY, O_RDWR)和文件创建标志(即O_CREAT,O_EXCL, O_NOCTTY, O_TRUNC)被忽略。在Linux上,这个命令只能改变O_APPEND, O_ASYNC, O_DIRECT, O_NOATIME和O_NON - BLOCK标志。无法更改O_DSYNC和O_SYNC标志;
RETURN VALUE
For a successful call, the return value depends on the operation:
F_GETFD Value of file descriptor flags.
F_GETFL Value of file status flags.
使用演示如下:
#include <stdio.h>
#include <stdlib.h>
#define TTYA "/dev/tty11"
#define TTYB "/dev/tty12"
relay(int fd1,int fd2)
{
int fd1_save,fd1_save;
//将这两个文件描述符都做成以非阻塞方式打开
fd1_save = fcntl(fd1,F_GETFL);
fcntl(fd1,F_SETFL,fd1_save|O_NONBLOCK);
fd2_save = fcntl(fd2,F_GETFL);
fcntl(fd2,F_SETFL,fd2_save|O_NONBLOCK);
}
int main()
{
int fd1,fd2;
fd1 = open(TTYA,O_RDWR);
if(fd1 < 0)
{
perror("open()");
exit(1);
}
fd2 = open(TTY2,O_RDWR|O_NONBLOCK);
if(fd2 < 0)
{
perror("open()");
exit(1);
}
relay(fd1,fd2);
close(fd1);
close(fd2);
return 0;
}
ioctl
管家级的函数,与设备相关的操作归它管。
/dev/fd目录
虚目录,显示的是当前进程的文件描述符信息。
如下图所示,现在是使用ls这个命令来看,所以显示的是ls这个命令实现所用到的文件描述符。
如何查看你正在运行的进程的文件描述符信息。
在程序的执行过程中打开/dev/fd/这个目录来查看。