Linux系统编程(三)文件系统

一、目录和文件

1.1 文件属性(stat)

stat() 可以通过文件名获取文件的属性

fstat() 可以通过打开的文件描述符获取文件的属性。

lstat() 和 stat() 功能相同,有一点区别就是当 pathname 是一个符号链接文件的时候,lstat() 返回的是符号链接文件本身的属性,而不是链接文件指向的文件的属性。而 stat() 则是返回符号链接所指向文件的属性。

#include 
#include 
#include 

int stat(const char *pathname, struct stat *statbuf);
int fstat(int fd, struct stat *statbuf);
int lstat(const char *pathname, struct stat *statbuf);

下面是 stat 结构体中的部分成员: 

struct stat {
               dev_t     st_dev;         /* ID of device containing file */
               ino_t     st_ino;         /* Inode number */
               mode_t    st_mode;        /* File type and mode */
               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;     /* Block size for filesystem I/O */
               blkcnt_t  st_blocks;      /* Number of 512B blocks allocated */

               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 */
}

我们可以通过 stat 结构体中的 st_size 成员获取文件长度,程序 flen.c 如下:

#include 
#include 
#include 
#include 
#include 

off_t flen(char *path){
        struct stat fstat;
        if (stat(path, &fstat) < 0) {
                perror("stat()");
                exit(1);
        }
        return fstat.st_size;
}


int main(int argc, char * argv[])
{
        if (argc < 2) {
                perror("flen ");
                exit(1);
        }

        off_t fsize = flen(argv[1]);
        printf("file size: %lld\n", (long long)fsize);
        exit(0);
}

但 st_size 仅仅是文件的属性,实际占用磁盘的块大小和个数是 st_blksize 和 st_blocks。如下图,文件 flen.c 的 Size 为 429 B,但是占用磁盘块数为 8个,8 * 512B = 4096B:

Linux系统编程(三)文件系统_第1张图片

下面是一段创建空洞文件的代码 hole_file.c,该程序会创建一个大小为 5g 的空洞文件(注意 5LL * 1024LL * 1024LL * 1024LL 一定要加单位 LL,否则计算结果会溢出): 

#include 
#include 
#include 
#include 
#include 
#include 

int main(int argc, char *argv[])
{
        if (argc < 2) {
                perror("hole_file ");
                exit(1);
        }

        int fd = open(argv[1], O_RDWR | O_CREAT);
        if(fd < 0){
                perror("open");
                exit(1);
        }

        /* make a 5g hole file */
        lseek(fd, 5LL * 1024LL * 1024LL * 1024LL, SEEK_SET);
        write(fd, "", 1);

        exit(0);
}

创建空洞文件 tmp 后使用 stat 查看其信息,可以看到 size 为 5g,但是实际在磁盘上仅占用了 8 个块,一共 4kb: 

Linux系统编程(三)文件系统_第2张图片

1.2 文件访问权限(umask)

使用 ls -l 查看文件的权限信息可以得到如下结果,第一位用于指出文件的类型,后 9 位分别指出 文件所有者的权限、同组用户的权限以及其他人的权限。这些内容存储在 stat 结构体的 st_mode 成员中,st_mode 是一个 16 位的位图,用于表示文件类型,文件访问权限,及特殊权限位。

文件类型分为如下几种:

  • d:目录文件;
  • c:字符设备文件;
  • b:块设备文件;
  • -:常规文件;
  • l:符号链接文件;
  • s:网络套接字(socket)文件;
  • p:pipe 管道文件;

umask() 函数可以设置文件创建时的权限,防止产生权限过松的文件。并且返回之前的 mask 值。

#include 
#include 

mode_t umask(mode_t mask);

1.4 文件权限的管理(chmod)

文件的权限位中,1 对应 x(执行),2 对应 w(可写),4 对应 r(可读),我们可以通过 chmod 指令来修改文件的权限位信息,如下:

Linux 操作系统也提供了 chmod() 和 fchmod() 函数来使得我们能在程序中修改文件的权限。

chmod() 可以通过文件路径修改文件的权限。

fchmod() 可以通过打开的文件描述符来修改文件权限。 

#include 

int chmod(const char *pathname, mode_t mode);
int fchmod(int fd, mode_t mode);

1.5 文件系统(FAT、UFS)

文件系统:文件或数据的存储和管理。

FAT16/32:静态单链表,闭源,惧怕大文件。

UFS:分区,位图,inode,块。

面试题:统计无符号整数二进制中 1 的个数。

去一法:

#include 

int main()
{
    unsigned int n;
    scanf("%u\n", &n);

    int count = 0;
    while (n != 0) {
        n = n & (n-1);
        count++;
    }

    printf("一的个数为:%d\n", count);
    return 0;
}

运行结果: 

Linux系统编程(三)文件系统_第3张图片

1.6 硬链接,符号链接(unlink、remove)

使用 ln 命令创建文件 file 的硬链接文件:

然后使用 stat 命令查看两个文件的信息:

Linux系统编程(三)文件系统_第4张图片

可以发现两个文件的 inode 号是相同的,并且硬链接数 links 数变成了 2。由此可以知,硬链接在目录下创建一个目录项,该目录项指向的 inode 与被链接的文件的 inode 相同,所以两个文件 file 和 file_link 对应的是同一个磁盘上的文件,两者本质上是同一个文件。

硬链接是目录项的同义词,并且建立硬链接有限制,不能跨分区建立,不能给目录建立。

然后再使用 ln -s 来创建 file 的符号链接:

Linux系统编程(三)文件系统_第5张图片

使用 stat 查看符号链接文件的信息,可以发现其指向的 inode 和原文件 file 不同,并且 links 为 1: 

Linux系统编程(三)文件系统_第6张图片

符号链接可跨分区,可给目录建立。 

其实符号链接类似于 windows 中的快捷方式,相当于重新创建了一个新文件,文件内容为被链接文件的路径,使用 readlink -f 可以查看符号链接本身的内容:

unlink 系统调用可以删除文件名所指向文件的硬链接,如果删除后文件硬链接数为 0,则文件被删除(从磁盘上删除),如果删除后硬链接数不为 0,则文件不被删除。

#include 

int unlink(const char *pathname);

另外,unlink 可以帮助我们创建一个临时文件,在程序中先打开一个新文件,然后立刻用 unlink 删除文件的硬链接,但此刻文件不会立刻被删除(因为进程打开了这个文件,链接数不为 0),进程结束后该文件才被释放。 

真正实现删除文件的函数是 remove(rm 命令就是用这个封装的): 

#include 

int remove(const char *pathname);

1.7 目录的创建和销毁(mkdir、rmdir)

mkdir 系统调用可以创建一个目录。

#include 
#include 

int mkdir(const char *pathname, mode_t mode);

rmdir 系统调用可以删除一个目录。 

#include 

int rmdir(const char *pathname);

1.8 更改当前工作路径(chdir)

chdir 系统调用可以更改当前进程的工作目录(cd 命令)。

#include 

int chdir(const char *path);
int fchdir(int fd);

getcwd 系统调用可以获取当前工作目录(pwd 命令)。 

#include 

char *getcwd(char *buf, size_t size);

1.9 分析目录/读取目录内容(glob、opendir、readdir、rewinddir、seekdir、telldir

glob(3) 函数可以找到所有与 pattern 所匹配的路径名(pattern 中一般包含通配符,如 * ? 等)。

#include 

int glob(const char *pattern, int flags,
         int (*errfunc) (const char *epath, int eerrno),
         glob_t *pglob);
void globfree(glob_t *pglob);

结构体 glob_t 存放返回结果,如下,gl_pathc 和 gl_pathv 类似于 argc 和 argv,gl_pathc 存放路径名的个数,gl_pathv 存放路径名的地址。

typedef struct {
    size_t   gl_pathc;    /* Count of paths matched so far  */
    char   **gl_pathv;    /* List of matched pathnames.  */
    size_t   gl_offs;     /* Slots to reserve in gl_pathv.  */
} glob_t;

例子(glob.c):使用 glob 函数找出当前目录下的所有 .c 文件,注意参数不能从 shell 中传,只能在程序内部定义,因为 shell 在传值之前会先进行通配符转换(如将 *.c 变成实际的文件路径)。 

#include 
#include 
#include 

static int errfunc(const char *epath, int eerrno)
{
  printf("%s\n", epath);
  return 0;
}

int main()
{
  glob_t pglob;

  if (glob("./*.c", 0, NULL, &pglob) != 0) {
    perror("glob");
    exit(1);
  }

  for (int i = 0; i < pglob.gl_pathc; i++) {
    printf("%s\n", pglob.gl_pathv[i]);
  }

  globfree(&pglob);
  exit(0);
}

运行结果如下: 

 Linux系统编程(三)文件系统_第7张图片

opendir(3)、closedir(3) 可以打开和关闭目录流。

#include 
#include 

DIR *opendir(const char *name);
DIR *fdopendir(int fd);

int closedir(DIR *dirp);

在通过 opendir(3) 打开目录后,可以使用 readdir(3) 函数通过 DIR 指针来读目录项。

#include 

struct dirent *readdir(DIR *dirp);

返回的 struct dirent 成员如下,并且存储在静态区中。  

struct dirent {
        ino_t          d_ino;       /* Inode number */
        off_t          d_off;       /* Not an offset; see below */
        unsigned short d_reclen;    /* Length of this record */
        unsigned char  d_type;      /* Type of file; not supported
                                       by all filesystem types */
        char           d_name[256]; /* Null-terminated filename */
};

例子(readdir.c):打印对应目录下的所有文件的名字。

#include 
#include 
#include 
#include 

int main(int argc, char *argv[])
{
  if (argc < 2) {
    perror("Usage: readdir ");
    exit(1);
  }

  DIR *dp;
  struct dirent *dirent_p;

  if ((dp = opendir(argv[1])) == NULL) {
    perror("opendir");
    exit(1);
  }

  while ((dirent_p = readdir(dp)) != NULL) {
    printf("%s\n", dirent_p->d_name);
  }

  exit(0);
}

rewinddir(3) 函数可以重置目录流 dirp 至目录的开头处。这样的话再使用 readdir(3) 函数就会读取第一个目录项。

#include 
#include 

void rewinddir(DIR *dirp);

seekdir(3) 函数可以设置下一次 readdir(3) 开始读取的位置,其中的 loc 参数必须是 telldir(3) 函数返回的值。

#include 

void seekdir(DIR *dirp, long loc);

telldir(3) 函数返回当前目录流的位置。

#include 

long telldir(DIR *dirp);

二、系统数据文件和信息

2.1 /etc/passwd

下面是 Ubuntu 下的 /etc/passwd 文件的内容,其格式如下:

用户名:加密后的口令:用户id:用户所在组id:注释字段:home目录位置:登录shell 

Linux系统编程(三)文件系统_第8张图片

getpwuid(3) 可以通过用户的 uid 获取用户的信息。

getpwnam(3) 可以通过用户的名称 name 获取用户信息。

#include 
#include 

struct passwd *getpwnam(const char *name);
struct passwd *getpwuid(uid_t uid);

struct passwd 结构体的定义如下(在 中,可以看到成员和 /etc/passwd 里面的内容差不多): 

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 */
};

/etc/group 文件可以获取组的信息,getgrgid(3) 和 getgrnam(3) 两个函数也可以获取组信息。

/etc/shadow 文件可以查看用户经过 hash 后的密码。

获取时间戳,time(2) 系统调用,gtime(3),localtime(3) 等。

Linux系统编程(三)文件系统_第9张图片

三、进程环境

3.1 进程的终止( 重要! )

正常终止

  1. 从 main 函数返回;
  2. exit(库函数);
  3. _exit 或 _Exit(系统调用);
  4. 最后一个线程从其启动例程返回;
  5. 最后一个线程调用 pthread_exit;

异常终止

  1. 调用 abort(得到一个 coredump 文件);
  2. 接到一个信号并终止;
  3. 最后一个线程对取消请求做出响应;

main 函数的返回值是给其父进程看的,如在 shell 中执行程序,其父进程为 shell,使用 echo $? 即可查看 shell 上一个程序执行的返回状态。

exit(3) 函数可以让进程正常终止。

#include 

void exit(int status);

atexit(3)钩子函数,进程正常终止时,被 atexit(3) 注册的函数会逆序调用(与注册顺序相反),类似于 C++ 的析构函数。

#include 

int atexit(void (*function)(void));

如下面的程序: 

Linux系统编程(三)文件系统_第10张图片

先打印 begin 和 end,直到程序 exit(0) 正常终止后,再依次调用 f3、f2、f1 函数: 

Linux系统编程(三)文件系统_第11张图片

要注意的是,在直接调用 _exit(2) 系统调用的时候,是不会执行钩子函数和 IO 清理的。而 exit(3) 则会依次执行,如下图。 

3.2 命令行参数的分析

getopt(3)、getopt_long(3) 分别可以获取 “-” 和 “--” 后的命令行参数。

3.3 C 程序的存储空间布局

C 程序的存储空间布局分为以下五个部分:

  1. 代码区
  2. 常量区
  3. 静态区,分为已初始化部分(.data)和未初始化部分(.bss)
  4. 堆区,由程序员动态分配和释放(malloc 和 free)
  5. 栈区,由编译器自动分配和释放

Linux系统编程(三)文件系统_第12张图片

Linux系统编程(三)文件系统_第13张图片

pmap 命令可以展示进程的内存分布图。 

3.4 库

  1. 动态库(.so 后缀),在目标代码运行时或加载时链接,生成的可执行文件小,只能在包含相应动态库的环境下运行,并且多个程序可以使用环境中的同一个动态库
  2. 静态库(.a 后缀),链接时会拷贝到目标代码中,生成可执行文件大,在不包含相应库的环境下也能运行,多个程序会有多个静态库的拷贝
  3. 手工装载库(使用 dlopen(3) 和 dlclose(3) 等函数在程序中动态装载和卸载动态库(shared object));

浅谈静态库和动态库 - 知乎

3.5 函数跳转

setjmp()、longjmp()。

你可能感兴趣的:(Linux系统编程,linux,学习,c语言)