【Linux系统调用API】三、进程地址虚拟空间、fcntl函数、stat函数

2024-08-08 17:06:10 浏览数 (2)

一、进程虚拟地址空间与文件描述符

首先我们看一下进程虚拟空间和文件描述符的示意图。

下面我们写一个程序来测试一下,一次性最多能打开的文件数量,来验证文件描述符的作用和范围。

代码语言:javascript复制
/************************************************************
 >File Name : openfilemax.c
 >Author     : QQ
 >Company   : QQ
 >Create Time: 2022年05月14日 星期六 10时25分25秒
************************************************************/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <fcntl.h>

int main(int argc, char* argv[])
{
int id = 3;
char filename[128] = {0};
while(1)
{
sprintf(filename, "file_d", id);
if(open(filename, O_CREAT | O_RDONLY, 0644) < 0)
{
perror("open err:");
break;
}
id  ;
}
printf("open file maxnum: %dn", id);
return 0;
}

编译运行,可以看到运行结果为1024,实际文件名最小是0003最大是1023。这是为什么呢?我们通过上面的文件描述符示意图可以看到,文件描述符最大是1023,从0到1023也就是总共1024个文件描述符。也就是说我们最多可以一次性打开1024个文件,再多的话就没有文件描述符可用了。这样的话,我们打开的文件从0003到1023,再加上标准输入0、标准输出1、标准错误2这三个文件,总共就是1024个文件。因为在开启一个进程的时候默认会打开标准输入输出和标准错误这三个文件,所以我们实际打开的文件只有1023-3 1=1021个文件,那么总共打开的文件个数就是1024个。

二、fcntl函数

1. 阻塞与非阻塞

阻塞可能会发生在read()函数读取设备、读取管道或读取网络的时候,因为某种情况需要等待,而不会立即返回,叫做阻塞。下面通过read()读设备来演示,比如读输入输出设备 /dev/tty 。

我们先写一个测试函数来看一下阻塞的效果,让read()函数读取标准输入输出设备tty的内容,如果标准输入输出没有内容的话,read()函数就会被阻塞,直到tty有内容了,才会继续执行。

代码语言:javascript复制
/************************************************************
  >File Name  : read_tty.c
  >Author     : QQ
  >Company    : QQ
  >Create Time: 2022年05月13日 星期五 22时22分00秒
************************************************************/
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>

int main()
{
    int fd = open("/dev/tty", O_RDONLY);
    char buf[100];
    memset(buf, 0, sizeof(buf));
    while(1)
    {
        int ret = read(fd, buf, sizeof(buf));
        printf("read return : %dn", ret);
        if(ret > 0)
        {
            printf("buf data : %sn", buf); 
        }   
        printf("test : Because of blocking, not looping.n");
    }
    close(fd);
    return 0;
}

这里的printf("test.......")这句话主要是为了证明程序在阻塞,而不是在无限循环,假如程序暂停时没有打印这句话,说明程序还没有执行到这句话。下面编译运行一下,看一下效果。

通过运行结果可以看到,程序会卡住不动,直到我们在标准输入输出设备上输入内容,程序才会执行一个循环,并把我们输入的内容读出来。这就是阻塞,read()因为tty为空读不到内容,所以阻塞等待tty有内容。在这种状态下可以按ctrl c来发信号退出。

这就是阻塞的效果,阻塞时整个程序卡在那不动,其实是很浪费资源的,下面我们看一下非阻塞的效果。下面先对之前的程序进行改动一下,通过open()函数的O_NONBLOCK参数来实现非阻塞打开文件。

代码语言:javascript复制
int main()
{
    int fd = open("/dev/tty", O_RDONLY | O_NONBLOCK); /*加O_NONBLOCK参数实现非阻塞*/
    char buf[100];
    memset(buf, 0, sizeof(buf));
    while(1)
    {
        int ret = read(fd, buf, sizeof(buf));
        if(ret < 0)
        {
            perror("read err: ");   /*打印出错信息,read函数返回失败的时候会设置errno*/
        }
        printf("read return : %dn", ret);
        if(ret > 0)
        {
            printf("buf data : %sn", buf); 
        }   
        printf("test : Because of blocking, not looping.n");
        sleep(2); /*睡眠2秒,不然的话会一直刷屏*/
    }
    close(fd);
    return 0;
}

非阻塞的情况下,如果read()函数读取不到内容,那么它不会阻塞在原地,而是会返回失败-1并设置errno 。因为是非阻塞,所以函数体内的循环会一直执行无限循环,所以要加一个睡眠函数,防止刷屏。

执行后可以看到,由于是非阻塞的方式打开tty设备,所以程序一直在循环执行,如果我们不向tty标准输入输出设备输入内容的话,read()会返回-1,并打印出一条出错信息 "Resource temporarily unavailable" ,然后继续执行下一次循环(如果是阻塞的方式打开文件,read()函数读取不到内容会暂停),如果我们输入内容,read()函数会把我们输入的内容读取出来并返回读取的字节数。

2. fcntl函数设置非阻塞

  • 包含头文件
代码语言:javascript复制
#include <unistd.h>
#include <fcntl.h>
  • 函数原型
代码语言:javascript复制
int fcntl(int fd, int cmd, ... /* arg */ );
  • 函数功能 fcntl() performs one of the operations described below on the open file descriptor fd. The operation is determined by cmd. 打开一个文件描述符,操作由cmd来决定。这个函数功能还是很多的,可以通过 man 2 fcntl 来查看。
  • 函数参数 它是一个可变参数的函数,... /* arg */ 的内容取决于cmd,比较常用的两个如下
    • 获取标志:F_GETFL (void) Read the file status flags; arg is ignored. 也就是说,如果cmd选择F_GETFL的话,后面参数可以为void,也就是没有第三个参数
    • 设置标志:F_SETFL (long) 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 only change the O_APPEND, O_ASYNC, O_DIRECT, O_NOATIME, and O_NONBLOCK flags. 这个的话,如果cmd是F_SETFL,那么后边参数应该是long类型的
  • 函数返回值 F_GETFL Value of flags. 如果我们使用的cmd参数是F_GETFL 那么就会把获取的标志返回出来。实际上,fcntl()函数的返回值也是由cmd参数来决定的,这只是比较常用的一个,更多的返回值可以在man手册查询。

下面我们通过实例来说明这个函数的用法,接上一节的话题,我们可以不在open()打开文件的时候设置非阻塞,而是在程序中使用fcntl()函数来设置非阻塞参数,具体代码如下。

代码语言:javascript复制
int main()
{
    int fd = open("/dev/tty", O_RDONLY);
    /*第一次调用,使用F_GETFL来获取标志,并通过返回值返回*/
    int flag = fcntl(fd, F_GETFL);
    /*把标志重新设置,通过或运算置非阻塞位*/
    flag |= O_NONBLOCK;
    /*第二次调用,使用F_SETFL参数来设置标志位,把上面修改好的标志在设置回文件*/
    fcntl(fd, F_SETFL, flag);
    char buf[100];
    memset(buf, 0, sizeof(buf));
    while(1)
    {
        int ret = read(fd, buf, sizeof(buf));
        if(ret < 0)
        {
            perror("read err: ");
        }
        printf("read return : %dn", ret);
        if(ret > 0)
        {
            printf("buf data : %sn", buf); 
        }   
        printf("test : Because of blocking, not looping.n");
        sleep(2); 
    }
    close(fd);
    return 0;
}

在这里通过fcntl()函数三行代码也实现了非阻塞打开文件的效果,和上面的open()直接加参数O_NONBLOCK效果是一样的。

这个函数功能是非常多的,这里只介绍用于设置非阻塞这一个功能,其他功能在后面用到时介绍。

三、stat函数

1. inode节点与硬链接

通过上图可以看到,硬链接和源文件引用的是同一个inode节点,并且在inode节点中有一条硬链接计数信息,每当inode被引用一次,这个硬链接计数就会加1,我们可以通过ls命令来查看inode节点信息。我们先建立一个文件以及该文件的硬链接,通过ll命令可以查看文件信息(实际上这些信息就是存在inode节点中的信息)。

可以看到,建立一个硬链接之后,硬链接计数增加了1个。通过ls命令的-i选项可以查看文件的inode节点编号。

硬链接文件和源文件的inode节点编号一样,说明它们引用的是同一个inode节点。

在上图中的目录项中,有一条信息是类型,如果当前是目录的话,可以继续进入下一级目录。简单举个例子,比如说我们使用vi打开当前目录可以得到下面的内容。

这里面的三个条目是当前文件夹下的文件,我们可以通过tree命令查看一下当前文件夹./的目录结构

当我们把光标停在某一文件所在行按回车键就可以查看该文件内容,如果这个文件是目录,就会进入该目录并显示目录下的条目。

比如,进入file.txt文件

进入目录111

2. stat函数与 struct stat 结构体

  • 包含头文件
代码语言:javascript复制
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
  • 函数原型
代码语言:javascript复制
int stat(const char *path, struct stat *buf);
int fstat(int fd, struct stat *buf);
int lstat(const char *path, struct stat *buf);
  • 函数功能 Get file status - These functions return information about a file.
  • 函数参数
    • 对于结构体struct stat中的 mode_t st_mode 进行简要介绍(下面并没有全部列出,只列出了部分),首先是判断文件类型的两种方法。第一种是掩码的方式。 S_IFMT 0170000 bit mask for the file type bit fields S_IFSOCK 0140000 socket S_IFLNK 0120000 symbolic link 符号链接 S_IFREG 0100000 regular file 普通文件 S_IFBLK 0060000 block device 块设备 S_IFDIR 0040000 directory 目录 S_IFCHR 0020000 character device 字符设备 S_IFIFO 0010000 FIFO 管道 这个实际上就是我们ll命令查看文件时,第一列所显示的文件类型,在上面列出的内容中,中间一列数字用来标识文件类型,从S_IFSOCK到S_IFIFO就是我们所熟知的7种文件类型,S_IFMT是文件类型的掩码。通过和S_IFMT的与运算,就可以把标识文件类型的位保留下来(也就是前两个数017,第一个0表示8进制,后面17共四位 1 111 用来标识文件类型,那么S_IFMT用二进制表示就是1111后面加12个0,通过与与运算就把后面12位置为0而保留代表文件类型的四个bit,再把相与的结果与下面的S_IFSOCK到S_IFIFO进行对比来判断文件类型),进而判断文件类型。比如,现在有一个0041100,把它和S_IFMT做与运算结果为 0041100 & 0170000 = 0040000 (S_IFMT) (S_IFDIR) 通过结果对比可以得出这是一个目录文件。这是通过掩码的方式来判断文件类型。 另外一种判断文件类型的方法是使用它为我们提供的宏来判断,7种文件类型判断相关的宏如下所示,这里的m是指stat结构体中的st_mode。 S_ISREG(m) is it a regular file? S_ISDIR(m) directory? S_ISCHR(m) character device? S_ISBLK(m) block device? S_ISFIFO(m) FIFO (named pipe)? S_ISLNK(m) symbolic link? (Not in POSIX.1-1996.) S_ISSOCK(m) socket? (Not in POSIX.1-1996.) 最后就是用户、组、其他用户的权限位(4位掩码,第一个0表示8进制) S_IRWXU 00700 mask for file owner permissions S_IRUSR 00400 owner has read permission S_IWUSR 00200 owner has write permission S_IXUSR 00100 owner has execute permission S_IRWXG 00070 mask for group permissions S_IRGRP 00040 group has read permission S_IWGRP 00020 group has write permission S_IXGRP 00010 group has execute permission S_IRWXO 00007 mask for permissions for others (not in group) S_IROTH 00004 others have read permission S_IWOTH 00002 others have write permission S_IXOTH 00001 others have execute permission
    • st_mode 各位含义示意图
    • 结构体struct stat中的时间time_t也是一个结构体,它的原型如下 struct timespec { _kernel_time_t tv_sec; /*seconds 当前时间到1970.1.1 0:0:0的秒数*/ long tv_nsec; /*nanoseconds 纳秒*/ }
    • path:指定文件
    • buf:buf是一个传出参数,也就是一级指针做输出,我们应该先定义一个结构体变量,并把该变量取地址&传给形参。 struct stat { dev_t st_dev; /* ID of device containing file */ 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 */ gid_t st_gid; /* group ID of owner */ dev_t st_rdev; /* device ID (if special file) */ off_t st_size; /* total size, in bytes 文件大小 */ blksize_t st_blksize; /* blocksize for file system I/O 块大小 */ blkcnt_t st_blocks; /* number of 512B blocks allocated 块的个数 */ time_t st_atime; /* time of last access 最后访问时间 */ time_t st_mtime; /* time of last modification 最后修改时间 */ time_t st_ctime; /* time of last status change 最后状态改变时间 */ };
  • 函数返回值 成功返回0,失败返回-1并设置errno 。On success, zero is returned. On error, -1 is returned, and errno is set appropriately.

3. stat函数实例分析及stat命令

下面通过一个实例来演示一下stat函数的使用方法。测试函数如下

代码语言:javascript复制
/************************************************************
  >File Name  : getstat.c
  >Author     : QQ
  >Company    : QQ
  >Create Time: 2022年05月14日 星期六 18时37分17秒
************************************************************/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>

int main(int argc, char* argv[])
{
    if(argc < 2)
    {
        printf("not found filenamen");
        return -1;  
    }
    struct stat m_status;
    stat(argv[1], &m_status);
    return 0;
}

我们可以借助gdb调试器来查看结构体内容,并且借助gdb强大的功能可以很优美的打印出结构体内容。

我们通过这个8进制数和文件类型掩码、权限掩码分别相与就可以得到这个文件的文件类型以及各用户的权限。

代码语言:javascript复制
S_IFMT & 0100644 = 0100000 ===>  S_IFREG    0100000

可以看到相与之后的值对应于S_IFREG,也就是说这是个普通文件。

继续往下看 st_uid=0,st_gid=0,这个对应的是用户及组ID,这里要说明的是,我当前使用的是root用户,相应用户的ID可以在 /etc/passwd 中查看,我们可以使用vim编辑器打开文件查看

代码语言:javascript复制
vim /etc/passwd

在第一行就可以看到root,后面两个0就对应结构体中的 st_uid=0和st_gid=0 。

继续往下看st_size = 11表示文件大小,可以通过ll命令来验证一下

再后面是块大小st_blksize = 4096和块的个数st_blocks = 8,那么每个块的大小是512,实际上这两个内容就是记录有多少个512大小的块。

再后面就是三个时间结构体的信息,最近访问时间、最近更改时间、最近状态改动时间。

  • Access 最近访问时间:是指最近的一次访问(读/写等),比如使用cat、touch等命令访问了该文件(访问但是没有修改),那么最近访问时间就会更新;
  • Modify 最近更改时间:是指最近一次文件内容的更改时间;
  • Change 最近状态改动时间:是指最近一次文件属性的更改时间,文件属性的更改包括文件大小、硬链接计数、文件权限等等的修改,并且一般Modify发生改变的时候,Change也会随之变化,因为文件内容的修改一般都会涉及到文件大小等的变化;

如果我们只是用cat查看一下文件,那么只有Access时间会更新;如果我们只是修改文件的权限,比如增加可执行权限,那么只有Change时间更新;如果我们使用重定向往文件中写入内容,那么Modify和Change时间会更新,而Access时间不会更新,因为在重定向的过程中,并没有访问文件。

实际上,上面介绍的这些内容,直接通过stat命令就可以查看

4. 实现 ls -l filename命令

我们可以通过stat函数来实现 ls -l 命令的功能,下面我们实现查看指定文件的 ls -l 命令,即

代码语言:javascript复制
ls -l filename

实现代码如下

代码语言:javascript复制
/************************************************************
  >File Name  : mls.c
  >Author     : QQ
  >Company    : QQ
  >Create Time: 2022年05月15日 星期日 16时29分01秒
  >实现目标:-rw-r--r--. 2 root root 11 5月  14 15:02 file.txt
************************************************************/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <time.h>
#include <pwd.h>
#include <grp.h>

int main(int argc, char* argv[])
{
    if(argc < 2)
    {
        printf("not found filenamen");
        return -1;
    }
    /*通过stat函数获取文件信息*/
    struct stat fstatus;
    stat(argv[1], &fstatus); /*穿透*/
    /*lstat(argv[1], &fstatus); 非穿透 */
    /*解析文件信息,st_mode st_uid st_gid time*/
    /*-rw-r--r--.*/
    char stmode[11];
    memset(stmode, '-', sizeof(stmode));
    /*使用宏来判断文件属性*/
    if(S_ISREG(fstatus.st_mode)) /* regular file */
    {
        stmode[0] = '-';
    }
    if(S_ISDIR(fstatus.st_mode))
    {
        stmode[0] = 'd';
    }
    if(S_ISCHR(fstatus.st_mode))
    {
        stmode[0] = 'c';
    }
    if(S_ISBLK(fstatus.st_mode))
    {
        stmode[0] = 'b';
    }
    if(S_ISFIFO(fstatus.st_mode))
    {
        stmode[0] = 'p';
    }
    if(S_ISLNK(fstatus.st_mode))
    {
        stmode[0] = 'l';
    }      
    if(S_ISSOCK(fstatus.st_mode))
    {
        stmode[0] = 's';
    }
    /*解析权限位*/
    if(fstatus.st_mode & S_IRUSR) 
    {/*为真表示拥有该权限,否则无权限,因为整块内存已初始化为 - 所以不需要else分支*/
        stmode[1] = 'r';
    }
    if(fstatus.st_mode & S_IWUSR) 
    {
        stmode[2] = 'w';
    }
    if(fstatus.st_mode & S_IXUSR) 
    {
        stmode[3] = 'x';
    }
    if(fstatus.st_mode & S_IRGRP) 
    {
        stmode[4] = 'r';
    }
    if(fstatus.st_mode & S_IWGRP) 
    {
        stmode[5] = 'w';
    }
    if(fstatus.st_mode & S_IXGRP) 
    {
        stmode[6] = 'x';
    }
    if(fstatus.st_mode & S_IROTH) 
    {
        stmode[7] = 'r';
    }
    if(fstatus.st_mode & S_IWOTH) 
    {
        stmode[8] = 'w';
    }
    if(fstatus.st_mode & S_IXOTH) 
    {
        stmode[9] = 'x';
    }
    stmode[10] = '';
    
    /*获取时间  localtime() 函数(非系统调用)
    *原型:struct tm *localtime(const time_t *timep);
    *参数:time_t类型,struct stat中time_t st_atime,这里应该是文件访问时间
    *返回: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)  如果要求实际年份,应加上1990
               int tm_wday;         day of the week sunday=0
               int tm_yday;         day in the year 
               int tm_isdst;        daylight saving time 
           };
    */
    struct tm* filetime = localtime(&fstatus.st_atim.tv_sec);
    char timebuf[20] = {0};
    sprintf(timebuf, "%d月  %d d:d", 
                    filetime->tm_mon   1, 
                    filetime->tm_mday, 
                    filetime->tm_hour, 
                    filetime->tm_min);
                    
    /*打印格式 -rw-r--r--. 2 root root 11 5月  14 15:02 file.txt*/
    printf("%s %ld %s %s %ld %s %sn", 
            stmode, 
            fstatus.st_nlink, 
            getpwuid(fstatus.st_uid)->pw_name, 
            getgrgid(fstatus.st_gid)->gr_name, 
            fstatus.st_size, 
            timebuf, 
            argv[1]);
            
    /* 两个函数(非系统调用)
    struct passwd *getpwuid(uid_t uid); 根据uid获取用户信息
    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;       real name 
               char   *pw_dir;         home directory 
               char   *pw_shell;       shell program 
           };
   struct group *getgrgid(gid_t gid); 根据gid获取组信息
   struct group {
               char   *gr_name;        group name 
               char   *gr_passwd;      group password 
               gid_t   gr_gid;         group ID 
               char  **gr_mem;         group members 
           };
    */
    
    return 0;
}

测试一下效果

5. 穿透与非穿透

上面介绍了stat函数并通过stat函数实现了 ls -l 命令的功能。我们上面演示了使用自己实现的 ./mls 查看文件信息,假如说使用 ./mls 查看一个链接文件是什么效果呢,下面演示一下。

通过对比我们可以看到,符号链接(软链接)file.txt.soft的实际大小是8,但是我们自己实现的 ./mls 命令显示的大小是11。实际上,原因是这样的,我们在实现 ./mls 命令的时候是基于stat函数来获取文件信息的,stat函数有一个特性就是在获取链接文件信息的时候会进行穿透,去追溯符号链接的源文件,也就是说我们通过上面的命令 ./mls file.txt.soft 获取到的大小实际上是源文件file.txt的大小,我们可以验证一下。

那么我们自己如何实现获取符号链接的实际大小呢,这就用到了非穿透函数lstat,只要把上面代码实现中的函数调用stat替换为lstat就可以了,下面测试一下。

代码语言:javascript复制
lstat(argv[1], &fstatus);

0 人点赞