LINUX系统编程--3 文件系统

linux系统编程--3 文件系统

三 文件系统

内容介绍

  • 目录和文件:获取文件属性、文件访问权限、umask、文件权限的更改/管理、粘住位、文件系统UFS和FAT、硬链接以及符号链接、utime、目录的创建和销毁、更改当前工作路径、分析目录/读取目录内容。
  • 系统数据文件和信息
  • 进程环境

本部分目标:

  • 实现myls!!!

1 关于shell命令

格式为:
cmd --长格式 -短格式 非选项的传参

完成本章后,可以完成一个作业作业:自己写一个类ls的myls程序(实际上ls的实现很大一部分依赖于从stat中获得的信息)

2 获取文件属性信息

stat、fstat、lstat
用法以及返回的字段含义见书。

补充几点:
stat除了是一个函数外,还是一个shell命令,stat+文件名,可以获得指定文件的文件属性信息。

利用stat完成获取文件大小的小例子(从stat中获取其他信息也是同理):

#include<stdio.h>
#include<stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>

// 计算文件的大小(利用stat)

static  int flen( const char* fname)
{
    struct stat statres;
   
    if(stat(fname,&statres)<0)
    {
        perror("stat()");
        exit(1);
    }

    return statres.st_size;
    
    
}

int main(int argc,char**argv)
{
    if(argc<2)
    {
        fprintf(stderr,"Useage error");
        exit(1);
    }
    printf("the file size is %d\n",flen(argv[1]));

    exit(0);
}

2 空洞文件与文件大小

首先说一下文件的inode,文件的inode号是文件的唯一标识,就像身份证号。对应于stat中得到的st_ino。

因为有空洞文件的存在,文件属性中的st_size实际上是不是文件实际占用磁盘的空间的大小!他只是文件的一个属性!文件实际的大小是stat结构体中的块大小和块个数确定的!

举个例子,下面的程序建立一个5GB大小的空洞文件,用ls -al或者stat得到文件的大小确实是5G,但是这个文件在磁盘中只占用4KB个字节的大小(占用8个块,在unix下每个块大小是512B)。

#include<stdio.h>
#include<stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
//得到一个空洞文件
int main(int argc,char**argv)
{
    int fd;

    if(argc<2)
    {
        fprintf(stderr,"Useage error");
        exit(1);
    }

    fd=open(argv[1],O_WRONLY|O_CREAT|O_TRUNC,0600);
    if(fd<0)
    {
        perror("open()");
        exit(1);
    }
    lseek(fd,5ll*1024ll*1024ll*1024ll-1ll,SEEK_SET);


    write(fd,"",1);
    close(1);

    exit(0);
}

编译后得到:
LINUX系统编程--3 文件系统

3 粘住位

t位
待补充



4 文件系统:FAT,UFS

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

FAT16/32:静态存储的单链表

UFS:这个文件系统APUE书上有原理!

具体的更多的关于文件系统的知识,在bird_linux上!

5 关于位图

位图是个数据结构
很多面试题是用这个数据结构解决的
位图相关操作特别多
这一块内容待补充





6 链接文件

硬链接的各种文件的i节点号都是一样的,因为都是指向同一个文件inode。硬链接的文件就是相当于普通文件,其文件类型也是普通文件。用ls和stat查看硬链接文件,得到的是其指向的inode文件的大小和内容。
硬链接和目录项是同义词!!且建立硬链接有限制,不能给分区建立,不能给目录建立。

注意一下符号链接:就是相当于windows快捷方式。
符号链接文件一般没有大小、不占用内存,(只是文件名会占用一点),(而硬链接文件的信息就是他所指向的inode节点文件的信息,有大小,有内容),i节点号也是单独的。符号链接文件是单独的一种文件类型。用ls和stat查看其属性,得到的是其本身的文件属性,得不到其指向的文件的信息。
符号链接优点:可跨分区,可以给目录建立。

7 utime

可更改文件的最后读的时间和最后修改的时间。

8 分析目录、读取目录内容

glob、opendir、closedir、readdir、rewinddir、seeksir、telldir

首先,如果当前目录有可执行程序main,在shell中使用如下命令:
./main *.c
那么 *.c并不会传入main的参数列表中,而是, *会被当成通配符,寻找当前目录所有以.c结束的文件名,作为命令行参数传入main中!!

1、下面说glob:
glob就是根据shell使用的规则,搜索与模式匹配的所有路径名。

例子:

#include<stdio.h>
#include<stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#include <glob.h>
// glob用法示例
#define PAT  "/etc/a*.conf"
int main(int argc,char** argv)
{
   glob_t globres;
   int err;
   int i;

    err=glob(PAT,0,NULL,&globres);
    if(err)
    {
        printf("ERROR code=%d\n",err);
        exit(1);
    }
    for( i=0;i<globres.gl_pathc;i++)
    {
        puts(globres.gl_pathv[i]);
    }
    globfree(&globres);
    exit(0);
}

上面的程序,是搜索/etc/目录下所有以a开头,以.conf结尾的所有文件并打印出来。(如果PAT是/etc/*,那么就是搜索/etc/目录下的所有文件并显示出来)

glob_t中有使用动态内存分配,所以在使用完glob后,要记得使用globfree!!

2、其他目录操作
opendir、closedir、readdir、rewinddir、seeksir、telldir

#include<stdio.h>
#include<stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#include <glob.h>
#include <dirent.h>

//获取etc目录下的所有文件

#define PAT  "/etc"
int main(int argc,char** argv)
{
  DIR *dp;
  struct dirent* cur;
  dp=opendir(PAT);
  if(dp==NULL)
  {
      perror("opendir()");
      exit(1);
  }
  while((cur=readdir(dp))!=NULL)
  { 
    puts(cur->d_name);
  }
  closedir(dp);
  exit(0);
}

主要注意的就是opendir的返回类型和readdir的返回类型。
目录也是文件!!,也会把etc下的目录的名字打印出来。(目录也有inode节点!)

3、目录解析实现实例
上面是两个是两种解析目录的方式(一种是glob通配符匹配,一种是目录操作)

补充:shell命令du,得到的是文件实际占用的磁盘大小,并且以KB为单位。一般来说一个block是512byte,也就是半K字节(du后得到的数字再乘2就是所占的块的个数);加上-b,得到的是以byte为单位的文件大小(就不再是文件实际占用的磁盘大小,而是得到文件大小这个文件属性)。
LINUX系统编程--3 文件系统LINUX系统编程--3 文件系统

实战
例1:自己实现shell的du命令(用glob)

#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <glob.h>
#include <string.h>

#define PATHSIZE 1024

static int path_noloop(const char *path) {
    char *pos;
    pos = strrchr(path,'/');
    if (pos == NULL) {
        exit(1);
    }

    if (strcmp(pos+1,".") == 0||strcmp(pos+1,"..")== 0) {
        return 0;
    }
    return 1;
}

static int64_t mydu(const char *path) {
    static struct stat statres;
    static char nextpath[PATHSIZE];
    glob_t globres;
    int64_t sum = 0;

    //非目录
    if (lstat(path,&statres) < 0) {
        perror("lstat()");
        exit(1);
    }

    if (!S_ISDIR(statres.st_mode)){
        fprintf(stdout,"%ld\t%s\n",statres.st_blocks / 2,path);
        return statres.st_blocks;
    }
    //目录
    //拼接路径
    strncpy(nextpath,path,PATHSIZE);
    strncat(nextpath,"/*",PATHSIZE);
    if (glob(nextpath,0,NULL,&globres) < 0) {
        fprintf(stderr,"glob()");
        exit(1);
    }

    strncpy(nextpath,path,PATHSIZE);
    strncat(nextpath,"/.*",PATHSIZE);
    if (glob(nextpath,GLOB_APPEND,NULL,&globres) < 0) {
        fprintf(stderr,"glob()");
        exit(1);
    }

    sum = statres.st_blocks;
    for (int i = 0;i < globres.gl_pathc;i++){
        if (path_noloop(globres.gl_pathv[i]))
            sum += mydu(globres.gl_pathv[i]);
    }
    
    globfree(&globres);//回收资源
    return sum;
        
}

int main(int argc,char **argv)
{   
    if (argc < 2) {
        fprintf(stderr,"%s\n","Usage...");
        exit(1);
    }
    
    printf("%ld\t%s\n",mydu(argv[1])/2,argv[1]);

    return 0;
}

例2:用目录操作实现du
自己完成

9 系统数据文件和信息

1、/etc/passwd
下面是书中的关于这个文件的字段的说明:
LINUX系统编程--3 文件系统
下面是文件中得到内容节选
LINUX系统编程--3 文件系统从左到右依次是表格中从上往下的内容。

getpwuid和getpwnam
在给出用户登录名或数值用户ID后,这两个函数就能查看passwd中的其他项。

2、/etc/group
也有两个函数,getgrgid和getrnam,用法和上面的两个函数相同。

3、/etc/shadow
LINUX系统编程--3 文件系统像之前说过的那样,尽管任何人包括root都没有权限,但是root用户可以绕过所有的权限!所以root还是可以打开这个文件查看或修改!!

LINUX系统编程--3 文件系统文件里的内容大概类似于上图所示。

getspnam函数也与上面的类似,传入一个系统用户名,得到一个结构体,里面包含有加密口令信息等内容。

实战:(检验用户的密码正确与否)

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <shadow.h>
#include <string.h>
#define _GNU_SOURCE         /* See feature_test_macros(7) */
#include <crypt.h>


int main(int argc,char **argv)
{
    char *input_passwd;//来自命令行的密码原文
    char *crypted_passwd;
    struct spwd *shadowline;
    
    if (argc < 2) {
        fprintf(stderr,"Usage...\n");
        exit(1);
    }

    input_passwd = getpass("PassWoed:");

    shadowline = getspnam(argv[1]);
    // printf("the crtpted password is :%s\n",shadowline->sp_pwdp);

    crypted_passwd = crypt(input_passwd,shadowline->sp_pwdp);
    //sp_padp,就是shadow文件中的第二个字段(一串乱码)
    //也就是说,用和系统同样的加密方法来加密所输入的字符串,然后再和系统的加密的密码进行比较
    
    if (strcmp(shadowline->sp_pwdp,crypted_passwd) == 0)
    
      puts("ok!");
    else
      puts("failed!");

    return 0;
}

上面的程序需要在root用户下进行,用系统中的某个用户名作为命令行参数传入,然后输入密码,可以检验该用户的密码正确与否。

4、时间戳
注意几个类型:time_t,char* ,struct tm
注意几个函数:time(),gmtime(), localtime(),mktime(), strftime().

先看date命令:

LINUX系统编程--3 文件系统(补充:每当用到指针时,都要弄明白指针所指的内存是在哪!静态区、堆还是栈)

实战1:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <shadow.h>
#include <string.h>
#include<time.h>
#define FNAME "./temp"
#define BUFSIZE 1024

int main()
{
    FILE *fp;
    char buf[BUFSIZE];
    int count =0;
    time_t stamp;
    struct tm *tm;

    while(1)
    {
      time(&stamp);
      tm=localtime(&stamp);
      fprintf(stdout,"%-4d %d-%d-%d %d:%d:%d\n",++count,\
      tm->tm_year+1900,tm->tm_mon+1,tm->tm_mday,tm->tm_hour,tm->tm_min,tm->tm_sec);

      sleep(1);
    }
    fclose(fp);
    
    exit(0);
}

实战2:求100天后是哪天

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <shadow.h>
#include <string.h>
#include<time.h>

#define TIMESTRSIZE 1024

int main()
{
    time_t stamp;
    struct tm *tm;
    char timestr[TIMESTRSIZE];
    
    time(&stamp);
    tm=localtime(&stamp);
    strftime(timestr,TIMESTRSIZE,"now :%Y-%m -%d",tm);
    puts(timestr);


    tm->tm_mday+=100;
    (void)mktime(tm);// 利用mktime的副作用,自动给我们换算时间!!
    strftime(timestr,TIMESTRSIZE,"after100 :%Y-%m -%d",tm);
    puts(timestr);
    exit(0);
}

10 进程环境

main函数
进程的终止
命令行参数的分析
环境变量
C程序的存储空间的布局

函数跳转
资源的获取及控制

1、 关于atexit()
钩子函数,很像C++中的析构函数

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <shadow.h>
#include <string.h>
#include<time.h>
#define TIMESTRSIZE 1024

static void f1(void)
{
    puts("f1 is working");
}
static void f2(void)
{
    puts("f2 is working");
}
static void f3(void)
{
    puts("f3 is working");
}

int main()
{
    puts("begin()");
    atexit(f1);
    atexit(f2);
    atexit(f3);
    //这三个函数都是注册用!,真正的调用是在exit执行的时候。本质上就是注册函数回调函数
    puts("end()");
    exit(0);
}

LINUX系统编程--3 文件系统
2、命令行参数与分析
关键函数:getopt、getopt_long

对于getopt,函数的作用是:解析命令行参数。它的参数argc和argv是程序调用时传递给main()函数的参数计数和数组。argv中以“-”开头的元素(不完全是“-”或“-”)是option元素。此元素的字符(除了首字母“-”)是选项字符。如果反复调用getopt(),它将依次返回每个选项元素中的每个选项字符。变量optind是要在中处理的下一个元素的索引argv。系统将此值初始化为1。调用者可以将其重置为1,以重新开始扫描相同的argv,或在扫描新的参数向量时。 如果getopt()找到另一个选项字符,它将返回该字符,并更新外部变量optind和静态变量nextchar,以便下次调用getopt()时可以使用以下选项字符或argv元素恢复扫描。 如果没有更多的选项字符,getopt()返回-1。然后optind是第一个不是选项的argv元素在argv中的索引。

命令行参数解析实战1:
实现的功能为,编写一个程序,按命令行参数传递的内容来显示时间。如:执行./mydate -y 显示当前年份;执行./mydate -m -y -d 依次显示当前的月份、年份、日份;执行./mydate -m -y -d -h,依次显示当前的月份、年份、日份、时间。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <shadow.h>
#include <string.h>
#include<time.h>

// -y 年
// -m 月
// -d day
// -H hour
// -M 分钟
// -S 秒
#define TIMESTRSIZE 1024
#define FMTSTRSIZE 1024
int main(int argc,char **argv)
{
    time_t stamp;
    struct tm *tm;
    char timestr[TIMESTRSIZE];
    int c;
    char fmtstr[FMTSTRSIZE];
    fmtstr[0]='\0';   
    time(&stamp);
    tm=localtime(&stamp);
    while(1)
    {
        c=getopt(argc,argv,"HMSymd");
        if(c<0)
            break;
        switch (c)
        {
        case 'H':
            break;
        case 'M':
            strncat(fmtstr,"%M ",FMTSTRSIZE);
            break;
        case 'S':
            strncat(fmtstr,"%S ",FMTSTRSIZE);
            break;
        case 'y':
            break;
         case 'm':
            strncat(fmtstr,"%m ",FMTSTRSIZE);
            break;
        case 'd':
            strncat(fmtstr,"%d ",FMTSTRSIZE);
            break;
        
        default:
            break;
        }
    }
    strftime(timestr,TIMESTRSIZE,fmtstr,tm);
    puts(timestr);    
    exit(0);
}

测试:
LINUX系统编程--3 文件系统连着和分开都是可行的;但是 - 必须要有!!

实战2:实现 选项参数
LINUX系统编程--3 文件系统
4和12分别是-y和-H的选项参数。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <shadow.h>
#include <string.h>
#include<time.h>
// -y 年
// -m 月
// -d day
// -H hour
// -M 分钟
// -S 秒
#define TIMESTRSIZE 1024
#define FMTSTRSIZE 1024
int main(int argc,char **argv)
{
    time_t stamp;
    struct tm *tm;
    char timestr[TIMESTRSIZE];
    int c;
    char fmtstr[FMTSTRSIZE];
    fmtstr[0]='\0';
    
    time(&stamp);
    tm=localtime(&stamp);

    while(1)
    {
        c=getopt(argc,argv,"H:MSy:md");
        if(c<0)
            break;
        switch (c)
        {
        case 'H':
            if(strcmp(optarg,"12")==0)
                strncpy(fmtstr,"%I(%P) ",FMTSTRSIZE);
            else if(strcmp(optarg,"24")==0)
                strncpy(fmtstr,"%H ",FMTSTRSIZE);
            else
                fprintf(stderr,"Invalid arg of H");
            break;
        case 'M':
            strncat(fmtstr,"%M ",FMTSTRSIZE);
            break;
        case 'S':
            strncat(fmtstr,"%S ",FMTSTRSIZE);
            break;
        case 'y':
            if(strcmp(optarg,"2")==0)
                strncpy(fmtstr,"%y ",FMTSTRSIZE);
            else if(strcmp(optarg,"4")==0)
                strncpy(fmtstr,"%Y ",FMTSTRSIZE);
            else
                fprintf(stderr,"Invalid arg of y");
            break;
         case 'm':
            strncat(fmtstr,"%m ",FMTSTRSIZE);
            break;
        case 'd':
            strncat(fmtstr,"%d ",FMTSTRSIZE);
            break;
        
        default:
            break;
        }
    }


    strftime(timestr,TIMESTRSIZE,fmtstr,tm);
    puts(timestr);
    
    exit(0);
}


区别就是这行代码:

c=getopt(argc,argv,“H:MSy:md”);

测试结果:
LINUX系统编程--3 文件系统LINUX系统编程--3 文件系统

例三: 实现 非选项参数
上面的-y和-H 后面必须有选项(也就是-y和-H是带选项的传参),这不是必须的。我们要实现非选项的传参以及-y和 -H后面不带参数也行。

下面要实现的是,实现非选项传参,即
LINUX系统编程--3 文件系统当后面有非选项参数时,结果放到后面非选项参数所指的文件中,若没有,则结果打印到终端上。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <shadow.h>
#include <string.h>
#include<time.h>

// -y 年
// -m 月
// -d day
// -H hour
// -M 分钟
// -S 秒
#define TIMESTRSIZE 1024
#define FMTSTRSIZE 1024
int main(int argc,char **argv)
{
    time_t stamp;
    struct tm *tm;
    char timestr[TIMESTRSIZE];
    int c;
    char fmtstr[FMTSTRSIZE];
    fmtstr[0]='\0';

    FILE* fp=stdout;
    
    time(&stamp);
    tm=localtime(&stamp);

    while(1)
    {
        c=getopt(argc,argv,"-H:MSy:md");
        if(c<0)
            break;
        switch (c)
        {

        case 1:
            fp=fopen(argv[optind-1],"w");
            if(fp==NULL)
            {
                perror("fopen()");
                fp=stdout;

            }
            break;

        case 'H':
            if(strcmp(optarg,"12")==0)
                strncpy(fmtstr,"%I(%P) ",FMTSTRSIZE);
            else if(strcmp(optarg,"24")==0)
                strncpy(fmtstr,"%H ",FMTSTRSIZE);
            else
                fprintf(stderr,"Invalid arg of H");
            break;
        case 'M':
            strncat(fmtstr,"%M ",FMTSTRSIZE);
            break;
        case 'S':
            strncat(fmtstr,"%S ",FMTSTRSIZE);
            break;
        case 'y':
            if(strcmp(optarg,"2")==0)
                strncpy(fmtstr,"%y ",FMTSTRSIZE);
            else if(strcmp(optarg,"4")==0)
                strncpy(fmtstr,"%Y ",FMTSTRSIZE);
            else
                fprintf(stderr,"Invalid arg of y");
            break;
         case 'm':
            strncat(fmtstr,"%m ",FMTSTRSIZE);
            break;
        case 'd':
            strncat(fmtstr,"%d ",FMTSTRSIZE);
            break;
        
        default:
            break;
        }
    }

    strncat(fmtstr,"\n",FMTSTRSIZE);
    strftime(timestr,TIMESTRSIZE,fmtstr,tm);
    fputs(timestr,fp);

    if(fp!=stdout)
        fclose(fp);
    
    exit(0);
}

需要注意的是这行代码:

c=getopt(argc,argv,"-H:MSy:md");

测试结果:
LINUX系统编程--3 文件系统

11 ls(实现myls)!!

实现myls的细节上面已经全部涉及到!所以下面实现myls:
。。。。
。。。。
。。。。

12 环境变量

1、export:shell命令,列出当前(进程)的所有的环境变量

注:若在shell中改变环境变量,只是修改的当前teminal的!关闭这个终端再重新打开一个新的,环境变量不会发生变动!
2 、关于environ
指向一个指针数组的首地址。也就是指向环境表的首地址。
在程序中使用environ实现和export相同的功能(打印出进程的所有环境变量):

#include <stdio.h>
#include <stdlib.h>

extern char **environ;//envrion是一个全局变量
int main()
{
    
    for (int i = 0;environ[i] != NULL;i++){
        puts(environ[i]);
    }
    return 0;
}

3、getenv和putenv以及setenv
见书

13 C程序的存储空间布局

没有新内容
只有一个pmap(1)

14库

动态库
静态库
手工装载库
这一部分也不是很懂,有待进一步了解
。。
。。
。。
。。
。。
。。

15 setjump和longjunp

没有新内容
见书

一个例子:
下面程序需要注意的是,setjunp调用一次,返回两次!

#include <stdio.h>
#include <stdlib.h>
#include <setjmp.h>

static jmp_buf save;

static void d(){
    printf("%s is called\n",__FUNCTION__);
    longjmp(save,2);
    printf("%s is returned\n",__FUNCTION__);
}


static void c(){
    printf("%s is called\n",__FUNCTION__);
    d();
    printf("%s is returned\n",__FUNCTION__);
}


static void b(){
    printf("%s is called\n",__FUNCTION__);
    c();
    printf("%s is returned\n",__FUNCTION__);
}

static void a(){
    int ret = setjmp(save);
    if  (ret == 0) {
        printf("%s is called\n",__FUNCTION__);
        b();
        printf("%s is returned\n",__FUNCTION__);
    }else {
        printf("code %d return \n",ret);
    }
}

int main()
{
    a();
    return 0;
}

运行结果:
LINUX系统编程--3 文件系统

16 资源的获取与控制

1、shell命令
ulimit -a 可以看到当前环境的资源上限,并可以看到更改某个资源上限的字符(比如可以看到n),那么执行ulimit -n 2048即可更改打开的文件上限为2048。

2 、getrlimit和setrlimit
ulimit就是由这两个函数封装的。

理解软限制与硬限制

上一篇:python标准库time 和datetime


下一篇:Streams AQ: qmn coordinator waiting for slave to start等待事件