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);
}
编译后得到:
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为单位的文件大小(就不再是文件实际占用的磁盘大小,而是得到文件大小这个文件属性)。
实战
例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
下面是书中的关于这个文件的字段的说明:
下面是文件中得到内容节选
从左到右依次是表格中从上往下的内容。
getpwuid和getpwnam
在给出用户登录名或数值用户ID后,这两个函数就能查看passwd中的其他项。
2、/etc/group
也有两个函数,getgrgid和getrnam,用法和上面的两个函数相同。
3、/etc/shadow
像之前说过的那样,尽管任何人包括root都没有权限,但是root用户可以绕过所有的权限!所以root还是可以打开这个文件查看或修改!!
文件里的内容大概类似于上图所示。
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命令:
(补充:每当用到指针时,都要弄明白指针所指的内存是在哪!静态区、堆还是栈)
实战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);
}
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);
}
测试:
连着和分开都是可行的;但是 - 必须要有!!
实战2:实现 选项参数
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”);
测试结果:
例三: 实现 非选项参数
上面的-y和-H 后面必须有选项(也就是-y和-H是带选项的传参),这不是必须的。我们要实现非选项的传参以及-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';
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");
测试结果:
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;
}
运行结果:
16 资源的获取与控制
1、shell命令
ulimit -a 可以看到当前环境的资源上限,并可以看到更改某个资源上限的字符(比如可以看到n),那么执行ulimit -n 2048即可更改打开的文件上限为2048。
2 、getrlimit和setrlimit
ulimit就是由这两个函数封装的。
理解软限制与硬限制