杨记

碎片化学习令人焦虑,系统化学习使人进步

0%

文件和目录操作

学自电子科技大学和黑马程序员

文档中的函数都可以用 man funcName 查看介绍

  • 系统调用使用 man 2 funcName 查看
  • 库函数使用 man 3 funcName 查看

Linux初识

UNIX和Linux

image-20220405150528083

C语言是UNIX的副产品!


80年代UNIX版本的剧增以及各种UNIX版本之间的差别不断扩大导致了以美国政府为代表许多用户要求对其标准化,以增强各种应用程序在这些UNIX操作系统之间的可移植性

重要的UNIX标准包括:ANSI C、IEEE POSIX等

UNIX标准只是对应用程序接口进行统一(内部实现机制则留给操作系统开发者自行实现)

ANSI C

  • ANSI C是美国国家标准协会(ANSI)于1983年发布的C语言标准
  • 1989年,此标准被采纳为国际标准ISO/IEC 9899:1990
  • ISO C标准现旨在提供应用程序的可移植性,使其能适应于不同的操作系统,而不仅仅是UNIX操作系统

POSIX

  • 1986年,IEEE制定了IEEE P1003标准,这套标准被称为POSIX(Potable Operating System Interface)
  • POSIX定义了一整套的应用程序接口,包括系统调用、库函数、公共命令
  • POSIX标准希望在源代码级别保证应用程序可移植性

Stallman在 1983 年发起了GNU计划,GNU是“GNU‘s Not Unix”的递归缩写,其目标是创建一个完全自由的类Unix操作系统,GNU计划也开发了大批其他的自由软件,例如 Emacs 、Glibc、GCC、BASH等

1985年Stallman又创立了自由软件基金会(Free Software Foundation)来为GNU计划提供技术、法律以及财政支持

自由软件并不是指“免费”的,而是指具有“自由度”的软件。什么是自由度呢?也就是使用者运行、复制、发布、研究、修改和改进该软件的自由

GNU通用公共许可协议(GNU GPL)是一个广泛被使用的自由软件许可协议条款,最初Stallman为GNU计划而撰写,GPL授予程序接受人以下权利,或称“自由”:

  • 以任何目的运行此程序的自由;
  • 再发行复制件的自由;
  • 改进此程序,并公开发布改进的自由

1984年”,安德鲁·斯图尔特·塔能鲍姆( Andrew Stuart Tanenbaum )自己编写了兼容于UNIX的Minix系统,用于教学

1991年,芬兰郝尔辛基大学研究生林纳斯·托瓦兹(Linus Torvalds)受Minix系统影响,开发了针对386机器的Linux内核

1991年Linux的第一个版本公开发行时,GNU计划已经完成除了操作系统内核之外的大部分软件(其中包括了shell程序,C语言程序库以及C语言编译器)。 Linus Torvalds及其他早期Linux开发人员加入了这些工具,而完成了Linux操作系统

Linux是在GNU通用公共许可证下发行,它却不是GNU计划的一部分


UNIX是可以应用从大型计算机到普通PC机等多种不同的平台上,是应用面最广、影响力最大的操作系统。

Linux是一种外观和性能与UNIX相同或更好的操作系统,但Linux不源于任何版本的UNIX的源代码,是一个类似于UNIX的产品

Linux遵循POSIX规范,成功的模仿了UNIX系统和功能,更具体地讲,Linux兼容于System V以及BSD UNIX:

  • 对于System V,应用程序源代码在Linux下重新编译之后就可以运行
  • 对于BSD UNIX,应用程序可执行文件可以直接在Linux环境下运行

Linux体系结构

image-20220405152119180

内核的功能

  • 内存管理
    • 内存分配调用: 包括静态分配方式、动态分配方式;
    • 内存保护:确保每个程序在自己的内存空间运行、互不干扰。方法是使用界限寄存器或存储保护键;
    • 地址映射:实现程序的逻辑地址与存储器的物理地址之间的映射功能;
    • 内存扩充:从逻辑上扩充物理内存,以允许比物理内存更大的程序在机器内运行,为此操作系统必须具有:请求调入功能与置换功能
  • 进程管理
    • 进程控制:包括进程创建、进程撤销、进程阻塞、进程唤醒
    • 进程协调:由于进程运行的异步性,因此进程同步的任务是对诸进程的运行协调,包括两种方式:进程互斥方式与进程同步方式;
    • 进程通信:主要完成同一台机器上不同进程间通信和不同机器上进程间的通信,以共同完成一相同的任务;
    • 进程调度:操作系统按照一定的规则对等待运行的多道程序进行调度,以保证每个程序都能有机会得到运行,并最终完成
  • 文件管理
    • 文件存储空间的管理:为每一文件分配必要的外存空间。为提高外部存储空间的利用率,系统应设置相应的数据结构,用于记录文件存储空间的使用情况;
    • 目录管理:为了方便对用户的文件进行管理,对文件系统建立一定结构的目录结构,同时要求快速的目录查询手段;
    • 文件的读、写管理和存取控制:利用一定的系统调用对文件进行读写操作。同时,为防止系统中的文件被非法访问和窃取,文件系统中必须提供有效存取控制功能;
  • 设备管理
    • 缓冲管理:管理各种类型的缓冲区,如字符缓冲区和块缓冲区,以缓和CPU和I/O速度不匹配的矛盾,最终达到提高CPU和I/O设备的利用率,进而提高系统吞吐量的目的;
    • 设备分配:根据用户的I/O请求,为之分配其所需要的设备;
    • 设备处理:又称为设备驱动程序,任务是实现CPU和设备控制器之间的通信;
    • 设备独立性和虚拟设备:一方面保证用户程序独立于物理设备,另一方面保证多个进程能并发地共享同一个设备;

UNIX/Linux操作系统结构

image-20220405152619972

系统调用与库函数


操作系统用户接口:

  • 命令接口:以命令形式呈现在用户面前,方便用户直接或间接控制自己的作业
  • 程序接口:为应用程序使用系统功能而设置,是应用程序取得操作系统服务的唯一途径。由一系列系统调用组成,每一个系统调用都是一个能完成特定功能的子程序。

  • 图形接口:采用了图形化的操作界面,将各种应用程序和文件,直观、逼真地表示出来。

系统调用是内核提供的程序接口,是应用程序和硬件设备之间的中间层

为应用程序提供了系统服务和硬件抽象能力,例如,当需要读文件时,应用程序可以不管磁盘类型和介质,甚至不用去管文件所在的文件系统到底是哪种类型;

系统调用保证了系统的稳定和安全

每个进程都运行在虚拟系统中

Research UNIX系统的第7个版本提供了大给50个系统调用,4.4BSD提供了大约110个,SVR4有大约120个。Linux根据不同的版本有240到260个系统调用的接口

  • man 2 syscalls 查看所有的系统调用接口

系统调用的类型:

  • 文件操作类系统调用: 如打开、创建、读取、删除、修改文件;
  • 进程控制类系统调用:如创建进程、设置或获取进程属性等;
  • 通信类系统调用:创建进程间的通信连接,发送、接收消息,或其他的通信方式;
  • 设备管理类系统调用:打开、关闭和操作设备;
  • 信息维护类系统调用:在用户程序和OS之间传递信息。例如,系统向用户程序传送当前时间、日期、操作系统版本号等。

系统调用与C库函数:

系统调用和C库函数之间并不是一一对应的关系,可能几个不同的函数会调用到同一个系统调用:

  • malloc函数和free函数都是通过sbrk系统调用来扩大或缩小进程的堆栈空间;

  • execl、execlp、execle、execv、execvp和execve函数都是通过execve系统调用来执行一个可执行文件

    image-20220405160247813

并非所有的库函数都会调用系统调用,例如,printf函数会调用write系统调用以输出一个字符串,但strcpy和atoi函数则不使用任何系统调用

C库IO函数的工作流程

image-20220527165704897

库函数和系统调用的关系

image-20220527165900165

UNIX/Linux软件层次架构:

image-20220405160358404

一般而言,应用程序使用API而不是直接使用系统调用来编程

进程UNIX/Linux的C库遵循POSIX规范,以库函数的形式实现了POSIX API(在API中使用系统调用完成相应功能)。

参考:/usr/include/asm/unistd.h (没有的话,用 find / -name "unistd.h"找一下,出来一大堆,感觉应该是/usr/include/unistd.h

image-20220405160535000

Linux命令

文件目录命令

浏览目录命令:lspwd

目录操作命令:cdmkdirrmdir

浏览文件命令:catmorelessheadtail

文件操作命令:cprmmvfindgreptar


命令名称:ls

  • 命令英文原意:list
  • 功能描述:显示目录文件
  • 语法:ls 选项[-ald] [文件或目录]
    • -a 显示所有文件,包括隐藏文件
    • -l 详细信息显示
    • -d 查看目录属性

命令名称:pwd

  • 命令英文原意:print working directory
  • 语法:pwd
  • 功能描述:显示当前所在的工作目录

命令名称:cd

  • 命令英文原意:change directory
  • 语法:cd [目录]
  • 功能描述:切换目录
  • 范例:
    • $ cd / 切换到根目录
    • $ cd .. 回到上一级目录

命令名称:mkdir

  • 命令英文原意:make directory
  • 语法:mkdir [目录名]
  • 功能描述:创建新目录
  • 范例: $ mkdir newdir

命令名称:rmdir

  • 命令英文原意:remove directory
  • 语法:rmdir [目录名]
  • 功能描述:删除空目录
  • 范例: $ rmdir newdir

命令名称:cat

  • 命令英文原意:concatenate and display files
  • 功能描述:连接文件并打印到标准输出设备上
  • 选项:
    • -E 在每一行的末尾显示$
    • -n 为显示行添加行号
  • 参数:指定要连接的文件列表。
  • 范例:
    • cat m1 (在屏幕上显示文件ml的内容)
    • cat m1 m2 (同时显示文件ml和m2的内容)

命令名称:more

  • 语法:more [文件名]
    • (空格)或f 显示下一页
    • (Enter) 显示下一行
    • q或Q 退出
  • 功能描述:分页显示文件内容
  • 范例:$ more /etc/services

less命令的用法与more命令类似,也可以用来浏览超过一页的文件。所不同的是less命令除了可以按空格键向下显示文件外,还可以利用上下键来卷动文件。

head命令:用于显示文件前几行的内容

  • 格式:head [参数] <文件名>
    • -n:显示前n行,不指定此参数显示前10行
    • [root@linux root]# head /etc/passwd

tail命令:用于显示文件后几行的内容

  • 格式:tail [参数] <文件名>
    • -n:显示后n行,不指定此参数显示后10行
    • +n:从第n行显示到文件尾
    • -F:用于跟踪显示不断增长的文件结尾内容(通常用于显示日志文件)。
    • [root@linux root]# tail /etc/passwd

cp(copy)命令可以将文件从一处复制到另一处。一般在使用cp命令时将一个文件复制成另一个文件或复制到某目录时,需要指定源文件名与目标文件名或目录。

  • 格式:cp [参数] <源文件路径> <目标文件路径>
    • -p :连同文件的属性一起复制,而非使用默认方式,常用于备份
    • -i :若目标文件已经存在时,在覆盖时会先询问操作的进行
    • -r :递归持续复制,用于目录的复制行为
    • -u :目标文件与源文件有差异时才会复制
  • 范例:
    • 将test1.txt复制成test2.txt:[test@linux test]$ cp test1.txt test2.txt
    • 将test3.txt复制到/tmp目录中:[test@linux test]$ cp test3.txt /tmp
    • 目录test1拷贝:[test@linux test]$ cp –r test1 test2

rm(remove)命令:删除文件或目录

  • 格式:rm [参数] <目标文件路径>
    • -f :就是force的意思,忽略不存在的文件,不会出现警告消息
    • -i :互动模式,在删除前会询问用户是否操作
    • -r :递归删除,最常用于目录删除,它是一个非常危险的参数
  • 范例:
    • 删除myfiles文件:[test@linux test]$ rm myfiles
    • 删除当前目录下的所有文件:[test@linux test]$ rm *
    • 递归删除某个目录所有内容:`[test@linux test]$ rm -r myfolder
    • 强迫删除所有后缀名为txt文件:[test@linux test]$ rm –f *.txt
    • 删除当前目录下的所有文件:[test@linux test]$ rm –i *(删除文件时会询问,可按Y或N键表示允许或拒绝删除文件)

find命令:用来寻找文件或目录

  • 格式:find 路径 [参数]
    • -name filename:找出文件名为filename的文件
    • -size [+-]SIZE :找出比SIZE还要大(+)或小(-)的文件
    • -tpye TYPE:查找文件的类型为TYPE的文件,TYPE的值主要有:一般文件(f)、设备文件(b、c)、目录(d)、连接文件(l)、socket(s)、FIFO管道文件(p);
    • -perm mode:查找文件权限刚好等于mode的文件,mode用数字表示,如0755

grep命令(Globally search a Regular Expression and Print)

  • 功能:在文件中搜索匹配的字符并进行输出
  • 格式:grep[参数] <要找的字串> <要寻找字串的源文件>
    • -a:将binary文件以text文件的方式查找数据
    • -c:计算找到“查找字符串”的次数
    • -i:忽略大小写的区别,即把大小写视为相同
    • -v:反向选择,即显示出不包含‘查找字符串’内容的那一行

tar(tape archive)命令能够将用户所指定的文件或目录打包成一个文件,也可以通过指定参数开启压缩/解压缩功能

  • 格式:tar [参数] <文件>
    • -c:新建打包文件
    • -t:查看打包文件的内容含有哪些文件名
    • -x:解打包或解压缩的功能,可以搭配-C(大写)指定解压的目录,注意-c,-t,-x不能同时出现在同一条命令中
    • -j:通过bzip2的支持进行压缩/解压缩
    • -z:通过gzip的支持进行压缩/解压缩
    • -v:在压缩/解压缩过程中,将正在处理的文件名显示出来
    • -f filename:filename为要处理的文件
    • -C dir:指定压缩/解压缩的目录dirdir必须已存在
  • 范例:

    • 将当前目录的所有文件打包成test.tar:[root@linux test]# tar -cvf test.tar *

    • 将当前目录的所有文件打包成test.tar,再用gzip命令压缩:[root@linux test]# tar -czvf test.tar.gz *

    • 查看test.tar文件中包括了哪些文件:[root@linux ljr]# tar -tf test.tar

    • 将test.tar解打包:[root@linux test]# tar -xvf test.tar

    • 将foo.tar.gz解压缩并解打包:[root@linux test]# tar -xzvf foo.tar.gz

    • -C的示例,打包Cprogram内的文件,不把Cprogram文件夹名也打包

      image-20220406120310070

进程控制类命令

查看系统中的进程命令:pstop

控制系统中的进程命令:killkillallnicerenice

进程后台运行命令 &

进程的挂起和恢复


程序和进程的区别

程序是一个包含可执行代码的文件,它放在磁盘等介质上。

当程序被操作系统装载到内存并分配给它一定资源后,此时可称为进程

程序是静态概念,进程是动态概念


进程状态

image-20220405170349612

ps命令是用来显示系统瞬间的进程信息,它可以显示出在用户输入ps命令时系统的进程及进程的相关信息。

  • 格式:ps [参数]
    • -l 长格式输出
    • -u 按用户名和启动时间的顺序来显示进程
    • -j 用任务格式来显示进程
    • -f 用树形格式来显示进程
    • -a 显示所有用户的所有进程(包括其它用户)
    • -x 显示无控制终端的进程
    • -r 显示运行中的进程

top命令

  • 功能:动态监视系统任务的工具,输出结果是连续的

  • 格式:top [参数]

    • -b 以批量模式运行,但不能接受命令行输入
    • -c 显示命令行,而不仅仅是命令名
    • -d N 显示两次刷新时间的间隔,比如-d 5,表示两次刷新间隔为5秒
    • -i 禁止显示空闲进程或僵尸进程
    • -n NUM 显示更新次数,然后退出。比如-n 5,表示top更新5次数据就退出
    • -p PID 仅监视指定进程的ID;PID是一个数值
    • -q 不经任何延时就刷新
    • -s 安全模式运行,禁用一些效互指令
    • -S 累积模式,输出每个进程的总的CPU时间
  • 范例:

    image-20220405171331260

kill命令,该命令用于向某个进程(通过PID标识)传送一个信号,它通常与psjobs命令一起使用

  • 格式:kill –signal PID,常用的signal参数如下:
    • 1:SIGHUP,启动被终止的进程
    • 2:SIGINT,相当于输入ctrl+c,中断一个程序的进行
    • 9:SIGKILL,强制中断一个进程的进行
    • 15:SIGTERM,以正常的结束进程方式来终止进程
    • 17:SIGSTOP,相当于输入ctrl+z,暂停一个进程的进行
  • 范例:
    • 以正常的结束进程方式来终止第一个后台工作进程 kill -SIGTERM %1
    • 重新启动进程ID为PID的进程 kill -SIGHUP PID

killall命令使用进程的名称来杀死进程,使用此指令可以杀死一组同名进程

使用kill命令可以杀死指定进程PID的进程,如果要根据进程名称找到需要杀死的进程,还需要在之前使用ps等命令再配合grep来查找进程,而killall把这两个过程合二为一

  • 用法:killall [参数] <正在运行的进程名>
    • -e:对长名称进行精确匹配
    • -I:忽略大小写的不同
    • -p:杀死进程所属的进程组
    • -i:交互式杀死进程,杀死进程前需要进行确认
    • -l:打印所有已知信号列表
    • -q:如果没有进程被杀死,则不输出任何信息
    • -r:使用正规表达式匹配要杀死的进程名称
    • -s:用指定的进程号代替默认信号“SIGTERM”
    • -u:杀死指定用户的进程
  • 使用范例:[root@localhost test]# killall game

nice命令:允许在默认优先级的基础上进行增大或减小的方式来运行命令

  • 格式:nice [参数] <command [arguments...]>
    • command 是系统中任意可执行文件的名称
    • -n, --adjustment 指定程序运行优先级的调整值
    • 优先级的调整值范围为-20 ~ 19 ,其中数值越小优先级越高,数值越大优先级越低
    • 若 nice命令未指定优先级的调整值,则以缺省值10来调整程序运行优先级,既在命令通常运行优先级基础之上增加10
  • 使用范例:[root@host root]# nice -n -5 myprogram&在后台以通常运行优先级-5的优先级运行myprogram

renice命令:改变一个正在运行的进程的nice

  • 格式:renice [参数] <pid>
    • -n 指定程序运行优先级的调整值
  • 使用范例:[root@host root]# renice -5 777 将正在运行的PID为777的进程nice值改为-5

后台运行程序的命令

  • [root@host root]# cp –r /usr/* test &将/usr 目录下的所有子目录及文件复制到/root/test目录下的工作放到后台运行

进程的中止(挂起)和终止

  • 挂起(Ctrl+Z)
  • 终止(Ctrl+C)

进程的恢复

  • 恢复到前台继续运行 fgfg [n]
  • 恢复到后台继续运行 bgbg [n]
  • 查看被挂起的进程 jobs

用户和权限管理

用户管理类命令:useraddusermodpasswduserdelsuidwhoamiwfinger

用户组管理类命令:groupaddgroupmodgroupdel

文件权限管理类命令:chmodchownchgrp

Linux用户分为三类:

  • 超级用户:拥有最高权限
  • 系统用户:与系统服务相关,但不能用于登录
  • 普通用户:由超级用户创建并赋予权限,只能操作其拥有权限的文件和目录,只能管理自己启动的进程

用户信息:

  • 用户名:唯一,由字母、数字和符号组成。
  • 口令
  • 用户ID(uid):每个用户拥有的唯一的识别号码。超级用户为0,系统用户1-499,普通用户从500开始
  • 用户组id(gid)
  • 用户主目录
  • 全称:用户帐户的附加信息,可以为空
  • 登录Shell:默认使用Bash

与用户相关的文件:

  • 用户帐号信息文件/etc/passwd
    • 文件中每一行为一个用户的信息
    • 文件中各字段从左到右依次为:用户名、口令、用户ID、用户组、全称、用户主目录和登录Shell
    • 口令字段用x来填充,加密后的口令保存在/etc/shadow文件中。
  • 用户口令信息文件/etc/shadow
    • shadow 文件只有超级用户才能查看并修改其内容,且加密(AES-256)存储。

用户组

  • Linux将相同特性的用户划归为同一用户组,可以大大简化用户的管理,方便用户之间文件的共享,任何用户都至少属于一个用户组。
  • 一个用户只能属于一个用户组,但可以同时属于多个附加组。用户不仅拥有其用户组的权限,还同时拥有其附加组的权限。
  • 用户组包括系统用户组与私人用户组

与用户组相关的文件:

  • 用户组账号信息文件/etc/group
    • 每一行为一个用户组信息
    • 文件中各字段从左到右依次为:用户组名、口令、用户组ID和附加用户列表
  • 用户组口令信息文件/etc/gshadow
    • /etc/shadow

useradd命令:新建用户帐号(超级用户可用)

  • 格式:useradd [参数] <用户名>
    • -d 指定用户登入时的主目录
    • -e 账号终止日期
    • -g 指定账户所属的用户组
    • -G 指定账户所属的附加组
    • -s 指定账户登录后所使用的shell
    • -u 指定用户ID号
  • 举例:新建一个用户zhangsan,用户组为net04:useradd –g net04 zhangsan

passwd命令:设置或修改用户的口令以及口令的属性

  • 格式:passwd [参数] <用户>
    • -d 删除用户的口令
    • -l 暂时锁定指定的用户帐号
    • -u 解除指定用户帐号的锁定
    • -s 显示指定用户帐号的状态
  • 范例:
    • 设置与修改属性 passwd zhangsan
    • 删除口令 passwd –d zhangsan
    • 锁定用户帐号 passwd –l zhangsan
    • 解锁用户帐号 passwd –u zhangsan
    • 显示用户帐号状态 passwd –s zhangsan

usermod命令:修改用户的属性(超级用户可用)

  • 格式:usermod [参数] <用户名>
    • -d 指定用户登入时的主目录
    • -e 账号终止日期
    • -g 指定账户所属的用户组
    • -G 指定账户所属的附加组
    • -s 指定账户登录后所使用的shell
    • -u 指定用户ID号
    • -l 新用户名(用于修改用户名)
  • 举例:将zhangsan改为zhangs :usermod –l zhangs zhangsan

userdel命令:删除指定的用户帐号(超级用户可用)

  • 格式:userdel [参数] <用户名>
    • -r:不仅删除此用户帐号,而且删除用户主目录及本地邮件存储的目录或文件
    • -f:删除用户登入目录以及目录中所有文件
  • 如果删除用户属于私人组群,而该组群没有其他用户,组群也一并删除。
  • 正在使用系统的用户不能删除。

su命令:切换用户身份

  • 格式:su <用户名>
    • 超级用户可以切换为任何普通用户,而不需要输入口令;普通用户转换为其他用户时需要输入被转换用户的口令
  • 使用exit可以返回到本来的用户身份

id命令:查看用户的UID、GID和用户所属用户组的信息,如果不指定用户,则显示当前用户的相关信息。

  • 格式:id <用户名>

whoami命令:查看当前用户名

w命令:查看当前登录系统用户和详细信息

groupadd命令:新建组群(超级用户可用)

  • 格式:groupadd [参数] <用户组名>
    • -g:指定用户组ID
    • -o:允许组ID号不唯一

groupmod命令:修改指定用户组的属性(超级用户可用)

  • 格式:groupmod [参数] <用户组名>
    • -g:指定新的用户组ID
    • -n:指定新的用户组名字
    • -o: 允许组ID号不唯一

groupdel命令:删除指定的用户组(超级用户可用)

  • 格式:groupdel <用户组名>
  • 注意:在删除指定用户组之前必须保证该用户组不是任何用户的主要组群,否则要先删除以此用户组为主要组群的用户才可以删除该用户组

文件权限

  • 读取权限:浏览文件/目录中内容的权限
  • 写入权限:
    • 对文件而言是修改文件内容的权限
    • 对目录而言是删除、添加和重命名目录内文件的权限
  • 执行权限:
    • 对可执行文件而言是允许执行的权限
    • 对目录而言是进入目录的权限。

文件用户分类

  • 文件所有者:建立文件和目录的用户
  • 文件所有者所在组用户:文件所有者所属用户组中的其他用户
  • 其他用户:既不是文件所有者,又不是文件所有组所在组的其他所有用户
  • 超级用户:负责整个系统的管理和维护,拥有系统中所有文件的全部访问权限。

image-20220405225155321


chmod命令:修改文件的访问权限

  • 格式:chmod <模式> <文件>
    • 模式:
      • 对象:u 文件所有者 、g 同组用户、o其他用户
      • 操作符:+增加、-删除、=赋予
      • 权限:r 读、w 写、x 执行、s设置用户ID
  • 举例:
    • 取消同组用户对file文件的写入权限 chmod g-w file
    • 将pict目录的访问权限设置为775 chmod 775 pict
    • 设置file文件的设置用户ID位 chmod u+s file

chown命令:将指定文件的拥有者改为指定的用户或用户组

  • 格式:chown [选项] <所有者/组> <文件>...
    • -c :显示更改的部分的信息
    • -f :忽略错误信息
    • -h :修复符号链接
    • -R :处理指定目录以及其子目录下的所有文件
    • -v :显示详细的处理信息
    • -deference :作用于符号链接的指向,而不是链接文件本身
    • 用户是用户名或者用户ID,用户组可以是组名或者组ID
    • 文件是以空格分开的要改变权限的文件列表,支持通配符
  • 举例:将ex1的所有者由root改为hellen:chown hellen ex1

chgrp命令:改变文件的所属用户组

  • 格式:chgrp [选项] <组> <文件>
    • -c :显示更改的部分的信息
    • -f :忽略错误信息
    • -h :修复符号链接
    • -R :处理指定目录以及其子目录下的所有文件
    • -v :显示详细的处理信息
    • -deference :作用于符号链接的指向,而不是链接文件本身
    • 用户组可以是组名或者组ID
  • 举例:将ex1文件所属的用户组由root改为staff:chgrp staff ex1

编译调试

gcc概述

gcc是GNU计划的一个项目。是一个自由编译器,如今的gcc已经是一个包含众多语言的编译器了( C,C++,Ada,Object C,Java及Go等)。所以,GCC也由原来的GNU C Compiler变为
GNU Compiler Collection

GCC主要包括:

  • cpp(预处理器)
  • gcc(c编译器)、g++(c++编译器)等编译器
  • binutils等二进制工具.
    • as(汇编器)
    • ld(链接器)

源文件-》预处理-》编译-》汇编-》链接-》可执行

  • cpp -o hello.i hello.c
  • ccl -o hello.s hello.i
  • as -o hello.o hello.s
  • ld -o hello hello.o

gcc基础使用方法:

  • gcc命令格式:gcc [选项] <文件名>
    • -o filename :指定输出文件为filename.该选项不在乎gcc产生什么输出,无论是可执行文件,目标文件,汇编文件还是预处理后的C代码
    • 如果没有使用-o选项,默认的输出结果是:可执行文件为a.out,编译后产生的目标文件是sourcename.o,汇编文件是 sourcename.s,而预处理后的C源代码送往标准输出
  • 对于源代码main.c,可以通过如下命令编译成最终可执行文件(默认包含了预处理、编译、汇编及链接四个阶段):gcc main.c –o main

gcc常用编译选项

  • -D: 宏定义选项,等同于代码中的#define MACRO ,但-D定义的宏作用于所有的源文件。
    • #define PI 3.14159(如果程序用到PI则用3.14159代替)
    • gcc -DPI=3.14159 main.c(但如果没有定义宏的话,就可以直接在编译的时候赋值再运行)
  • -I 头文件的搜索路径:如果用户的头文件不在gcc的搜索路径中,可以用此选项指定额外搜索路径。

    • gcc helloworld.c –I /usr/include –o helloworld(将/usr/inlcude加入到文件头文件的搜索路径中)
  • 警告选项

    • 警告是针对程序结构的诊断信息,程序不一定有错误,而是存在风险,或者可能存在错误。
    • 所有以-W开头的选项基本上均可使用-Wno-option来关闭该警告信息
    • -Wunused在某个局部变量除了声明就没再使用,或者声明了静态函数但是没有定义,或者某条语句的运算结果显然没有使用时, 编译器就发出警告。使用`-Wno-unused可禁止该警告信息。
    • -w :禁止所有警告信息.
    • -Wall:打开所有警告选项,输出警告信息
    • 通常建议打开-Wall,这样至少可以看出你的代码里有哪些地方可能存在问题

静态库编译和使用

  • .c编译成 .ogcc –c increase.c –o increase.o
  • .o归档成 静态库.aar –r libincrease.a increase.o
  • 静态库和其它源文件链接成可执行文件:gcc main.c –L –static –o main

动态库编译

  • 生成动态链接库:gcc -shared -fPIC -o libinc.so increase.c
    • 动态链接库的名字必须以lib开头 .so结束,这是linux系统上的强制约束,否则无法使用该共享库
    • -shared 生成共享文件
    • -fPIC 生成位置独立的代码,此类代码可以在不同进程间共享。

动态库的使用

  • -l library 名字为library的动态链接库。事实上此动态链接库在文件系统中的名字为liblibrary .so。连接器会自动加上lib*.so。
  • -L dir 共享库搜索目录。gcc除了会在自定义的目录中搜索共享库外,用户也自定义目录让gcc搜索。gcc main.c -o main -linc -L./

gdb概述

gdb是GNU计划开发的程序调试工具

gdb可以完成以下四个方面的功能:

  • 启动程序,可以按照自定义的要求随心所欲的运行程序
  • 可让被调试的程序在所指定的调置的断点处停住(断点可以是条件表达式)
  • 当程序被停住时,可以检查此时程序中所发生的情况
  • 动态的改变程序的执行环境

gdb的启动

  • 直接在shell中运行gdb命令,进入gdb界面后用file program装载程序。
  • 在shell中启动gdb并加载可执行文件gdb <program>
  • 用gdb同时调试一个运行程序和core文件(core是程序非法执行后core dump后产生的文件)gdb program core
  • 调试正在运行的进程gdb program <processid>
    • 进入gdb后用 attach <processid> 调试正在运行的进程。

gdb常用命令

break命令:

  • 功能:断点设置命令break(缩写 b),当gdb执行到该断点时会让程序暂停运行。此时程序员可以查看运行中程序的情况。
  • 格式:break [LOCATION] [thread THREADNUM] [if CONDITION]
    • [LOCATION]
      • linenum(行号),如b 123
      • function(函数名) ,如b main
      • filename:linenum,如b increase:123
      • filename:function,如b increase:main
      • class:function(c++)
    • [thread THREADNUM] 调试多线程程序时,切换到哪个线程或者在那个线程中设置断点。break frik.c:13 thread 28
    • [if CONDITION]: 当条件满足时,断点才生效。一般称为条件断点。CONDITION跟C语言一样。
      • b 123 if index==2当index为2时,程序在123行停下

watchpoint命令:

  • watchpoint称为观察点,当观察对象的值有变化时,程序立即停止执行。
  • watch <expr> :为表达式(变量)expr设置一个观察点。一旦表达式值有变化时,马上停住程序。
  • rwatch <expr> :当表达式(变量)expr被读时,停住程序。
  • awatch <expr> :当表达式(变量)的值被读或被写时,停住程序。
  • info watchpoints :列出当前所设置了的所有观察点

清除禁止断点或观察点:

  • clear [linenum] [function name] 清除所有断点,不会清除watchpoints
  • delete <num> 清除编号为num的断点或者watchpoint
  • disable <num> 禁止某个断点
  • enable <num> 开启某个断点

gdb调试命令:

  • step 单步调试命令,一次执行一行程序。
  • next 单步调试命令,但跳过函数调用。
  • finish 单步调试时直接从一个函数中返回
  • disassemble 显示汇编代码。
  • backtrace或者bt 查看目前程序的堆栈情况。
  • where查看当前位置。
  • up/down 向上或者向下移动一个堆栈。
  • frame<num>或者f 移动到第num个堆栈。
  • 当移动到某个堆栈时,便可以用gdb命令查看在那个堆栈中的局部变量。

Linux文件系统

一切皆文件

文件系统是以合理有效的层次结构组织的文件和目录的集合

一切皆是文件”是 Unix/Linux 的基本哲学之一

普通文件、目录、字符设备、块设备、 套接字等在 Unix/Linux 中都是文件

类型不同的文件都是通过相同的API对其进行操作

image-20220406135159849

Unix/Linux 中允许不同的文件系统共存,如 ext2, ext3, ext4, xfs, btrfs 等

文件系统 适用场景 原因
ext2 U盘 ext2不写日志,对安全性要求不高,兼容FAT
ext3 对稳定性要求高的地方 ext4稳定性不高
ext4 小文件较少 不支持inode动态分配
xfs 小文件多 支持iNode动态分配
btrfs 没有频繁的写操作 功能众多

通过统一的文件 I/O 系统调用API即可对系统中的任意文件进行操作而无需考虑其所在的具体文件系统格式

文件操作可以跨文件系统执行

image-20220406135352137

与windows文件系统的区别

Unix/Linux文件系统 Windows文件系统
根目录文件系统 多根目录文件系统
文件名区分大小写 文件名不区分大小写
目录路径使用正斜杠,如/home/name 目录路径使用反斜线,如C:\Users\zxy
没有驱动器号,一切文件都在root目录下 驱动器号标记分区和设备
无独占访问权限 独占访问权限

文件系统架构

image-20220406135923462

VFS

虚拟文件系统VFS

  • 虚拟文件系统是Linux 内核中的一个软件层,对内实现文件系统的抽象,允许不同的文件系统共存,对外向应用程序提供统一的文件系统接口
  • 为了能够支持不同文件系统,VFS 定义了所有文件系统都支持的基本的、抽象的接口和数据结构
  • 实际文件系统实现VFS 定义的抽象接口和数据结构,将自身的诸如文件、目录等概念在形式上与VFS的定义保持一致,在统一的接口和数据结构下隐藏了具体的实现细节

image-20220406140531667

VFS中的数据结构:

  • 超级块(super block):用于存储文件系统的控制信息的数据结构。描述文件系统的状态、文件系统类型、大小、区块数、索引节点数等,存放于磁盘的特定扇区中。
  • 索引节点(inode):用于存储文件的元数据(文件的基本信息)的一个数据结构,包含诸如文件的大小、拥有者、创建时间、磁盘位置等信息。
  • 目录项(dentry):目录被用来容纳文件,目录可以包含子目录,层层嵌套以形成文件路径。
  • 文件对象(file):一组在逻辑上具有完整意义的信息项的系列

super block

VFS-超级块(super block):

  • 超级块用来描述整个文件系统的信息。每个具体的文件系统都有各自的超级块
  • VFS超级块是各种具体文件系统在安装时建立的,并在卸载时被自动删除,其数据结构是super_block
  • 所有超级块对象以双向环形链表的形式链接在一起
1
2
3
4
5
6
7
8
9
10
struct super_block {
.....
kdev_t s_dev; // 具体文件系统的块设备标识符
unsigned long s_blocksize; // 以字节为单位数据块的大小
unsigned char s_blocksize_bits; // 块大小的值占用的位数
struct list_head s_list; // 指向超级块链表的指针
struct file_system_type *s_type; // 指向文件系统的file_system_type 数据结构的指针
struct super _operations *s_op; // 指向具体文件系统的用于超级块操作的函数集合
.....
}

inode

VFS-索引节点(inode)

  • 文件系统处理文件所需要的所有信息都放在称为索引节点的数据结构inode中
  • 具体文件系统的索引节点是存放在磁盘上的,是一种静态结构,要使用它,必须调入内存,填写VFS的索引节点,因此,也称VFS索引节点是动态节点
  • 文件名可以随时更改,但是索引节点对文件是唯一的,并且随文件的存在而存在
  • 每个inode节点的大小,一般是128字节或256字节
1
2
3
4
5
6
7
8
9
10
11
12
13
struct inode {
......
struct list_head i_hash; // 指向哈希链表的指针
struct list_head i_list; // 指向索引节点链表的指针
struct list_head i_dentry; // 指向目录项链表的指针
struct inode_operations *i_op; // 指向对该节点操作的函数
struct super_block *i_sb; // 指向该文件系统超级块的指针
atomic_t i_count; // 当前使用该节点的进程数
struct file_operations *i_fop; // 指向文件操作的指针
unsigned long i_state; // 索引节点的状态标志
unsigned int i_flags; // 文件系统的安装标志
......
}

dentry

VFS-目录项对象(dentry)

  • 每个文件除了有一个索引节点inode数据结构外,还有一个目录项dentry数据结构。
  • dentry结构代表的是逻辑意义上的文件,描述的是文件逻辑上的属性,目录项对象在磁盘上并没有对应的映像
  • inode结构代表的是物理意义上的文件,记录的是物理上的属性,对于一个具体的文件系统,其inode结构在磁盘上就有对应的映像
  • 一个索引节点对象可能对应多个目录项对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
struct dentry {
......
atomic_t d_count; // 目录项引用计数器
unsigned int d_flags; // 目录项标志
struct inode *d_inode; // 与文件名关联的索引节点
struct dentry *d_parent; // 父目录的目录项
struct list_head d_hash; // 目录项形成的哈希表
struct list_head d_lru; // 未使用的 LRU 链表
struct list_head d_child; // 父目录的子目录形成的链表
struct list_head d_subdirs; // 该目录项的子目录所形成的的链表
struct list_head d_alias; // 索引节点别名的链表
int d_mounted; // 目录项的安装点
struct qstr d_name; // 目录项名(可快速查找)
struct dentry_operations *d_op; // 操作目录项的函数
struct super_block *d_sb; // 目录项树的根(即文件的超级块)
unsigned long d_vfs_flags;
void *d_fsdata; // 具体文件系统的数据
unsigned char d_iname[DNAME_INLINE_LEN]; // 短文件名
......
}

file

VFS-文件对象(file)

  • 进程是通过文件描述符来访问文件的
  • Linux中专门用了一个file文件对象来保存打开文件的文件位置,这个对象称为打开的文件描述(open file description)
  • 文件描述符是用来描述打开的文件的。每个进程用一个files_struct结构来记录文件描述符的使用情况,这个files_struct结构称为用户打开文件表,它是进程的私有数据
  • file结构中主要保存了文件位置,此外,还把指向该文件索引节点的指针也放在其中。file结构形成一个双链表,称为系统打开文件表。
1
2
3
4
5
6
7
8
9
10
11
12
13
struct file {
struct list_head f_list; // 所有的打开的文件形成的链表
struct dentry *f_dentry; // 与该文件相关的dentry
struct vfsmount *f_vfsmnt; // 该文件在这个文件系统中的安装点
struct file_operations *f_op; // 文件操作
atomic_t f_count; // 引用计数
unsigned int f_flags; // 打开文件时指定的标识
mode_t f_mode; // 文件的访问模式
loff_t f_pos; // 目前文件的相对开头的偏移
unsigned long f_reada, f_ramax, f_raend, f_ralen, f_rawin;
// 预读标志、要预读的最多页面数、上次预读后的文件、预读的字节数以及预读的页面数
struct fown_struct f_owner; // 进程ID,信号
}
1
2
3
4
5
6
7
8
9
10
11
12
13
struct files_struct {
atomic_t count; // 引用计数
rwlock_t file_lock; // 锁,保护下面的字段
int max_fds; // 当前文件对象的最大的数量
int max_fdset; // 文件描述符最大数
int next_fd; // 已分配的最大的文件描述符
struct file **fd; // 指向文件对象指针数组的指针
fd_set *close_on_exec; // 执行exec()时候需要关闭的文件描述符
fd_set *open_fds; // 指向打开的文件描述符的指针
fd_set close_on_exec_init; // 执行exec()时需要关闭的文件描述符初始化值
fd_set open_fds_init; // 文件描述符初值集合
struct file *fd_array[NR_OPEN_DEFAULT]; // 文件对象指针的初始化数组
}

VFS数据结构之间的关系

  • 超级块是对一个文件系统的描述
  • 索引节点是对一个文件物理属性的描述
  • 目录项是对一个文件逻辑属性的描述
  • 一个进程所处的位置是由fs_struct来描述的,而一个进程(或用户)打开的文件是由files_struct来描述的,而整个系统所打开的文件是由file结构来描述

image-20220406150131249

文件系统的注册和注销

  • 当内核被编译时,就已经确定了可以支持哪些文件系统,这些文件系统在系统引导时,在 VFS 中进行注册。
  • VFS的初始化函数用来向VFS注册,即填写文件注册表file_system_type数据结构
  • 注册调用register_filesystem()函数
  • 注销即删除一个file_system_type 结构,需调用 unregister_filesystem()函数

image-20220406150315979

1
2
3
4
5
6
7
8
struct file_system_type {
const char *name; /*文件系统的类型名*/
int fs_flags; /*文件系统的一些特性*/
struct super_block *(*read_super)
(struct super_block *, void *, int); /*文件系统读入其超级块的函数指针*/
struct module *owner; /*确定是否把文件系统作为模块来安装*/
struct file_system_type *next;
};

文件系统的安装

  • 安装一个文件系统实际上是安装一个物理设备
  • 自己(一般是超级用户)安装文件系统时,需要指定三种信息:文件系统的名称、包含文件系统的物理块设备、文件系统在已有文件系统中的安装点。
  • $ mount -t iso9660 /dev/hdc /mnt/cdrom 其中,iso9660是光驱文件系统的名称,/dev/hdc是包含文件系统的物理块设备,/mnt/cdrom就是将要安装到的目录,即安装点。
  • 在用户程序中要安装一个文件系统则可以调用mount()系统调用。安装过程主要工作是创建安装点对象,将其挂接到根文件系统的指定安装点下,然后初始化超级块对象,从而获得文件系统基本信息和相关的操作。

文件系统的卸载

  • 如果文件系统中的文件当前正在使用,该文件系统是不能被卸载的
  • 查看对应的 VFS 超级块,如果该文件系统的 VFS 超级块标志为“脏”,则必须将超级块信息写回磁盘
  • 之后,对应的 VFS 超级块被释放,vfsmount 数据结构将从vfsmntlist 链表中断开并被释放
  • 具体的实现代码为fs/super.c中的sys_umount()函数

文件IO操作

系统调用:操作系统提供给用户程序调用的一组“特殊”接口,用户程序可以通过这组“特殊”接口来获得操作系统内核提供的服务

image-20220406152559930

Q:为什么用户程序不能直接访问系统内核提供的服务?

A:为了更好地保护内核空间,将程序的运行空间分为内核空间和用户空间(也就是常称的内核态和用户态),它们分别运行在不同的级别上,在逻辑上是相互隔离的。因此,用户进程在通常情况下不允许访问内核数据,也无法使用内核函数,它们只能在用户空间操作用户数据,调用用户空间的函数。

进行系统调用时,程序运行空间从用户空间进入内核空间,处理完后再返回到用户空间

image-20220406152730069

系统调用并不是直接与程序员进行交互的,它仅仅是一个通过软中断机制向内核提交请求,以获取内核服务的接口。

在实际使用中程序员调用的通常是用户编程接口——API

image-20220406152820289

Linux中的系统调用包含在Linux的libc库中,通过标准的C函数调用方法可以调用

image-20220406152854242

系统命令相对API更高了一层,它实际上是一个可执行程序,它的内部调用了用户编程接口(API)来实现相应的功能

image-20220406152959593

文件描述符

Q:内核如何区分和引用特定的文件?

A:通过文件描述符。文件描述符是一个非负的整数,它是一个索引值,并指向在内核中每个进程打开文件的记录表。当打开一个现存文件或创建一个新文件时,内核就向进程返回一个文件描述符;当需要读写文件时,也需要把文件描述符作为参数传递给相应的函数。

一个进程启动时,通常会打开3个文件:

  • 标准输入 描述符为 0
  • 标准输出 描述符为 1
  • 标准出错处理 描述符为 2

文件描述符和PCB

image-20220527165756007

文件IO函数

  • open() 用于打开或创建文件,可以指定文件的属性及用户的权限等各种参数
  • creat() 打开一个文件,如果文件不存在,则创建它
  • close() 用于关闭一个被打开的文件。当一个进程终止时,所有被它打开的文件都由内核自动关闭,很多程序都使用这一功能而不显示地关闭一个文件
  • read() 用于将从指定的文件描述符中读出的数据放到缓存区中,并返回实际读入的字节数。若返回0,则表示没有数据可读,即已达到文件尾。读操作从文件的当前指针位置开始
  • write() 用于向打开的文件写数据,写操作从文件的当前指针位置开始。对磁盘文件进行写操作,若磁盘已满或超出该文件的长度,则write()函数返回失败

open函数

查看详细信息 man 2 open

头文件

  • #include <sys/types.h>
  • #include <sys/stat.h>
  • #include <fcntl.h>

函数原型

  • int open(const char* pathname, int flags);
  • int open(const char* pathname, int flags, mode_t mode);

函数说明

  • 参数pathname 指向欲打开的文件路径字符串。
  • 下列是参数flags 常用的标识:
    • O_RDONLY只读方式打开文件
    • O_WRONLY只写方式打开文件
    • O_RDWR可读写方式打开文件。上述三种标识是互斥的,也就是不可同时使用,但可与下列的标识利用OR(|)运算符组合。
    • O_CREAT 若欲打开的文件不存在则自动建立该文件。
    • O_TRUNC 若文件存在并且以可写的方式打开时,此标识会令文件长度清为0,而原来存于该文件的资料也会消失。
    • O_APPEND 当读写文件时会从文件尾开始移动,也就是所写入的数据会以附加的方式加入到文件后面。
    • O_NONBLOCK 以不可阻断的方式打开文件,也就是无论有无数据读取或等待,都会立即返回进程之中。
    • O_EXCL: 如果同时指定 O_CREAT,而该文件又是存在的,报错;也可以测试一个文件是否存在,不存在则创建。
  • mode:文件权限,如0664
  • 返回值:文件打开成功返回文件的描述符,失败返回-1

creat函数

man 2 creat

头文件

  • #include<sys/types.h>
  • #include<sys/stat.h>
  • #include<fcntl.h>

函数原型:int creat(const char * pathname, mode_t mode)

  • 参数pathname指向欲建立的文件路径字符串。
  • creat()相当于使用下列的调用方式调用open(const char * pathname ,(O_CREAT|O_WRONLY|O_TRUNC))
  • creat()会返回新的文件描述词,若有错误发生则会返回-1,并把错误代码设给errno

附加说明

  • creat函数的一个不足之处是它以只写方式打开所创建的文件

close函数

man 2 close

头文件:#include<unistd.h>

函数原型:int close(int fd);

函数说明

  • 当使用完文件后若已不再需要则可使用close()关闭该文件,close()会让数据写回磁盘,并释放该文件所占用的资源。
  • 参数fd为先前由open()或creat()所返回的文件描述符;
  • 返回值:若文件顺利关闭则返回0,发生错误时返回-1。
  • 附加说明:虽然在进程结束时,系统会自动关闭已打开的文件,但仍建议自行关闭文件,并检查返回值。

read函数

man 2 read

头文件:#include<unistd.h>

函数原型:ssize_t read(int fd, void * buf, size_t count);

函数说明

  • read()会把参数fd 所指的文件传送count个字节到buf指针所指的内存中。
  • 若参数count为0,则read()不会有作用并返回0。
  • 返回值为实际读取到的字节数,如果返回0,表示已到达文件尾或是无可读取的数据,此外文件读写位置会随读取到的字节移动;
  • 返回值ssize_t(有符号整型):成功返回读取的字节数,出错返回-1;若返回0表示文件读完;

read函数实际读到的字节数少于要求读的字节数时:

  • 读普通文件,在读到要求字节数之前就到达文件尾
  • 当从终端设备读,通常一次最多读一行;
  • 当从网络读时,网络中的缓冲机构可能造成返回值小于所要求读的字节数;
  • 某些面向记录的设备,如磁带,一次最多返回一个记录;

读操作完成后,文件的当前位置将从读之前的位置加上实际读的字节数

当有错误发生时则返回-1,错误代码存入errno中,而文件读写位置则无法预期

write函数

man 2 write

头文件:#include<unistd.h>

函数原型:ssize_t write (int fd, const void * buf, size_t count);

函数说明

  • write()会把参数buf所指的内存写入count个字节到参数fd所指的文件内。当然,文件读写位置也会随之移动;
  • 返回值:如果顺利write()会返回实际写入的字节数。当有错误发生时则返回-1,错误代码存入errno中。
  • write出错的原因可能是磁盘满、没有访问权限、或写超过文件长度限制等等
  • 附加说明:将数据写入已打开的文件内。对于普通文件,写操作从文件当前位置开始写(除非打开文件时指定了O_APPEND选项)。写操作完成后,文件的当前位置将从写之前的位置加上实际写的字节数。

注意:数据无法一次性读完时:

  • 第二次读buf中数据时,读位置指针并不会自动移动
  • 按如下格式实现读位置移动:write(fp, p1+len, (strlen(p1)-len),直至指针恢复

Write一次可以写的最大数据范围是8192

  • 写入数据大小最好小于buff中的值
  • Count参数值大于SSIZE_MAX,则write调用的结果未定义
  • Count参数值为0时,write调用会立即返回0这个值
  • Write调用返回时,内核已经将缓冲区所提供的数据复制到内核的缓冲区,但是无法保证数据已经写出到预定的目的地

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#define FILE_PATH "./test.txt"

int main(void)
{

int fd;
if((fd = open(FILE_PATH, O_RDWR | O_CREAT | O_EXCL, 0666)) < 0)
{
printf("open error\n"); //test.txt文件存在的话会报错
exit(-1);
}
else
{
printf("open success\n");
}
printf("请输入内容到test.txt文件中\n");
char buf[1024];
int num = 0;
if ((num = read(STDIN_FILENO, buf, 200)) == -1)
{
printf ("read error");
exit(-1);
}
else
{
// 将键盘输入又输出到屏幕上
write(STDOUT_FILENO, buf, num);

//将键盘输入 分两次 输出到test.txt文件
int front = 0;
if((front = write(fd, buf, 10)) == -1)
{
printf("write error\n");
exit(-1);
}
printf("write %d Bytes\n", front);
if((front = write(fd, buf+10, num-10)) == -1)
{
printf("write error\n");
exit(-1);
}
printf("write %d bytes\n", front);
}
close(fd);
return 0;
}

ioctl函数

man 2 ioctl

设备驱动程序中对设备的I/O通道进行管理

头文件:#include<sys/ioctl.h>

函数原型:int ioctl(int fd, int cmd, ...);

函数说明

  • ioctl()能对一些特殊的文件(主要是设备)进行一些底层参数的操作。许多字符设备都使用ioctl请求来完成对设备的控制;
  • 返回值:成动返回0。当有错误发生时则返回-1,错误代码存入errno
  • 附加说明:ioctl是设备驱动程序中对设备的I/O通道进行管理的函数。所谓对I/O通道进行管理,就是对设备的一些特性进行控制,例如串口的传输波特率、马达的转速等等

image-20220406165728902

在驱动程序中实现的ioctl函数体内,实际上是有一个switch{case}结构,每一个case对应一个命令码,做出一些相应的操作

ioctl中命令码是唯一联系用户程序命令和驱动程序支持的途径

image-20220406165833211

“幻数”是一个字母,数据长度也是8,用一个特定的字母来标明设备类型

文件标准IO

为什么要设计标准I/O库?

  • 直接使用API进行文件访问时,需要考虑许多细节问题,例如:read、write时,缓冲区的大小该如何确定,才能使效率最优
  • read和write等底层系统调用函数进行输入输出时,在用户态和内核态之间来回切换,每次读出或写入的数据量较少,导致频繁的I/O操作,增加了系统开销

标准I/O库是ANSI C规范的一部分,函数原型在文件stdio.h中定义,对底层I/O系统调用进行了封装,为程序员提供了带有格式转换功能的输入输出操作,并在用户空间增加了缓冲区管理

标准I/O库

  • 分离了应用程序空间和实际的物理设备
  • 减少了直接读盘次数,提高性能
    • 读取前查看是否已存在页缓存中,如果已经存放在了页缓存中,数据立即返回给应用程序
    • 写数据前先写到页缓存中,如果用户采用的是同步写机制(synchronous writes),那么数据会立即被写回到磁盘上,应用程序会一直等到数据被写完为止;如果用户采用的是延迟写机制( deferred writes ),那么应用程序就完全不需要等到数据全部被写回到磁盘,数据只要被写到页缓存中去就可以了

fopen函数

man 3 fopen

fopen函数功能:打开一个指定文件

函数原型

  • FILE *fopen(const char *restrict pathname, const char *restrict type);

参数

  • pathname:要打开的文件名
  • type:指定文件的读、写方式
type 说明
r或rb 为读而打开
w或wb 使文件长度为0,或为写而创建
a或ab 追加文件内容,或为写而创建
r+或r+b或rb+ 为读写而打开
w+或w+b或wb+ 使文件长度为0,或为读写而打开或创建
a+或a+b或ab+ 在文件尾读写而打开或创建

setbuf函数

man 3 setbuf

定义流 stream 应如何缓冲

函数原型

  • void setbuf(FILE *steam, char *buf);

参数

  • stream — 这是指向 FILE 对象的指针,该 FILE 对象标识了一个打开的流。
  • buffer — 这是分配给用户的缓冲,它的长度至少为 BUFSIZ 字节,BUFSIZ 是一个宏常量,表示数组的长度。
1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>

int main()
{
char buf[BUFSIZ];

setbuf(stdout, buf);
puts("This is runoob");

fflush(stdout);
return(0);
}

C 库函数 – setbuf() | 菜鸟教程 (runoob.com)

setvbuf函数

man 3 setvbuf

函数原型

  • void setvbuf(FILE *steam, char *buf, int mode, size_t size);

参数

  • stream — 这是指向 FILE 对象的指针,该 FILE 对象标识了一个打开的流。
  • buffer — 这是分配给用户的缓冲。如果设置为 NULL,该函数会自动分配一个指定大小的缓冲。
  • mode — 这指定了文件缓冲的模式:
模式 描述
_IOFBF 全缓冲:对于输出,数据在缓冲填满时被一次性写入。对于输入,缓冲会在请求输入且缓冲为空时被填充。
_IOLBF 行缓冲:对于输出,数据在遇到换行符或者在缓冲填满时被写入,具体视情况而定。对于输入,缓冲会在请求输入且缓冲为空时被填充,直到遇到下一个换行符。
_IONBF 无缓冲:不使用缓冲。每个 I/O 操作都被即时写入。buffer 和 size 参数被忽略。
  • size —这是缓冲的大小,以字节为单位。

返回值:如果成功,则该函数返回 0,否则返回非零值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <stdio.h>
#include <unistd.h>
#include <string.h>
int main()
{

char buff[1024];

memset( buff, '\0', sizeof( buff ));

fprintf(stdout, "启用全缓冲\n");
setvbuf(stdout, buff, _IOFBF, 1024);

fprintf(stdout, "这里是 runoob.com\n");
fprintf(stdout, "该输出将保存到 buff\n");
fflush( stdout );

fprintf(stdout, "这将在编程时出现\n");
fprintf(stdout, "最后休眠五秒钟\n");

sleep(5);

return(0);
}

C 库函数 – setvbuf() | 菜鸟教程 (runoob.com)

fdopen函数

man 3 fdopen

函数功能

  • 取一个现存的文件描述符,并使一个标准I/O流与该描述符相结合

头文件

  • #include<stdio.h>

函数原型

  • FILE *fdopen(int fd, const char *type);

fdopen常用于由创建管道及网络通信通道函数返回的描述符。

  • 这些特殊类型的文件,不能用fopen打开
  • 因此必须先调用设备专用函数以获得一个文件描述符,然后再用fdopen使一个标准I/O流与该描述符相关联

对于fdopen函数,type参数的意义稍有区别

  • 因为该描述符已被打开,所以fdopen为写而打开并不截短该文件
  • 不能用于创建该文件(因为如若一个描述符引用一个文件,则该文件一定已经存在)
Type值 操作文件类型 是否新建文件 是否清空原文件 可读 可写 读写开始位置
r 文本文件 NO NO YES NO 文件开头
r+ 文本文件 YES NO YES YES 文件开头
w 文本文件 YES YES NO YES 文件开头
w+ 文本文件 YES YES YES YES 文件开头
a 文本文件 NO YES NO YES 文件结尾
a+ 文本文件 NO YES YES YES 文件结尾
rb 二进制文件 NO NO YES NO 文件开头
r+b或rb+ 二进制文件 YES NO YES YES 文件开头
wb 二进制文件 YES YES NO YES 文件开头
w+b或wb+ 二进制文件 YES YES YES YES 文件开头
ab 二进制文件 NO YES NO YES 文件结尾
a+b或ab+ 二进制文件 NO YES YES YES 文件结尾
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main()
{
FILE *fp;
int fd;
if ((fp = fopen("hello.txt", "w+")) == NULL)
{
printf("fopen file error\n");
return 0;
}
fprintf(fp, "hello word\n"); //写入文件
fclose(fp);

if((fd = open("hello.txt", O_RDWR)) == -1)
{
printf("open file fail\n");
return 0;
}

if((fp = fdopen(fd, "a+")) == NULL)
{
printf("fdopen open\n");
return 0;
}
fprintf(fp, "linux c program\n"); //写入文件
fclose(fp);
return 0;
}

文件定位

lseek函数

man 2 lseek

lseek函数用于改变文件的当前偏移量。

头文件

  • #include<unistd.h>
  • #include <sys/types.h>

定义函数

  • off_t lseek(int filedes, off_t offset, int origin);

函数说明

  • filedes 文件描述符
  • offset 必须与origin一同解析
  • originSEEK_SET, 则offset从文件的开头算起。
  • originSEEK_CUR, 则offset从当前位置算起,既新偏移量为当前偏移量加上offset
  • originSEEK_END, 则offset从文件末尾算起。

返回值

  • 如果失败,返回值为-1
  • 成功返回移动后的文件偏移量
    • 确定文件当前偏移量: currpos = lseek(fd, 0, SEEK_CUR);
    • 获取文件大小 size = lseek(fd, 0, SEEK_END);

lseek常用于找到文件的开头、找到文件的末端,判定文件描述符的当前位置

lseek仅将当前的文件偏移量记录在内核中,它并不引起任何I/O操作,然后该偏移量用于下一次读、写操作。

文件偏移量可以大于文件的当前长度,但并不改变相应的i节点信息。在这种情况下的下一次写将延长该文件(文件拓展),并在文件中构成一个空洞,但文件大小并不是文件的最大偏移量。对空洞位置的读操作将返回0。

lseek实现空洞

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <sys/types.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>

int main(void)
{
int fd;
int ret;
fd=open("hole.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
if(fd == -1)
{
printf("open error\n");
}
write(fd,"hello",5);
ret = lseek(fd, 1024*1024*1024, SEEK_CUR);
if(ret == -1)
{
printf("lseek error\n");
}
write(fd,"world",5);
close(fd);
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
yang@Ubuntu18~$ gcc lseek_test.c -o lseek_test
yang@Ubuntu18~$ ./lseek_test
yang@Ubuntu18~$ ls -lh hole.txt
-rw-r--r-- 1 yang yang 1.1G Apr 6 20:49 hole.txt
yang@Ubuntu18~$ od -c hole.txt
0000000 h e l l o \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0
0000020 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0
*
10000000000 \0 \0 \0 \0 \0 w o r l d
10000000012
yang@Ubuntu18~$ cat hole.txt
hello

pread函数

man 2 pread

在给定的偏移量读取一个文件描述

头文件

  • #include <unistd.h>

函数原型

  • ssize_t pread (int fd, void *buf, size_t count, off_t pos)

返回值

  • 返回读到的字节数;出错:返回-1;到文件结尾:返回0

解决问题

  • 由于lseek和read调用之间,内核可能会临时挂起进程,所以对同步问题造成了问题,调用pread相当于顺序调用了lseek 和read,这两个操作相当于一个捆绑的原子操作

其他

  • 调用pread时,无法中断其定位和读操作,另外不更新文件指针

pwrite函数

man 2 pwrite

作用:在给定偏移量处读取或写入文件描述符

头文件

  • #include<unistd.h>

函数原型

  • ssize_t pwrite (int fd, const void *buf, size_t count, off_t pos )

返回值

  • 返回已写的字节数;出错:返回-1

解决问题

  • 由于lseek和write 调用之间,内核可能会临时挂起进程,所以对同步问题造成了问题,调用pwrite相当于顺序调用了lseek 和 write,这两个操作相当于一个捆绑的原子操作

其他

  • 调用pwrite时,无法中断其定位和读操作,另外不更新文件指针

对比

pread/pwriteread/write的区别

  • 调用更容易使用,特别是在进行需要技巧的操作时
  • 完成工作后不会改变文件指针
  • 避免使用lseek时可能造成的竞争条件
    • 如果有多个线程共享文件描述符,当地一个线程调用lseek之后,在它进行读取或写入操作之前,同一个程序中的另一个线程可能会改变文件的位置

文件共享

UNIX/Linux支持不同进程间共享文件。内核使用的三种表(文件描述符表、文件表、索引结点表)之间的关系决定了在文件共享方面一个进程对另一个进程可能产生的影响

  • 每个进程在进程表中有一个文件描述符表,每个描述符表项指向一个文件表
  • 内核为每一个被打开的文件维护一张文件表,文件表项包含

    • 文件的状态标志(读、写、同步、非阻塞)
    • 文件当前位置
    • 指向该文件索引节点表的指针
  • 每个文件(或设备)都有一个索引节点,它包含了文件类型属性及文件数据

如果两个进程分别打开同一个的文件(物理文件),则它们有不同的文件表,因此每个进程有自己的文件当前位置,因此其读写操作互不影响。

也存在不同进程共享同一个文件表(父子进程),或同一进程共享同一个文件表(dup操作)。此时,两个进程对该文件的读写操作将基于同一个文件当前位置。

进程共享文件

image-20220407103144896image-20220407103144896

image-20220407103144896

dup和dup2

man 2 dup

头文件:unistd.h

函数原型

  • int dup( int oldfd );
  • int dup2( int oldfd, int targetfd );

函数说明:复制一个文件的描述符,dup2函数跟dup函数相似,但dup2函数允许调用者规定一个有效描述符和目标描述符的

返回值:dup2函数成功返回时,目标描述符(dup2函数的第二个参数)将变成源描述符(dup2函数的第一个参数)的复制品

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// dup.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>


int main()
{
int fd = open("a.txt", O_RDWR);
if(fd == -1)
{
perror("open");
exit(1);
}

printf("file open fd = %d\n", fd);

// 找到进程文件描述表中 ==第一个== 可用的文件描述符
// 将参数指定的文件复制到该描述符后,返回这个描述符
int ret = dup(fd);
if(ret == -1)
{
perror("dup");
exit(1);
}
printf("dup fd = %d\n", ret);
char* buf = "你是猴子派来的救兵吗????\n";
char* buf1 = "你大爷的,我是程序猿!!!\n";
write(fd, buf, strlen(buf));
write(ret, buf1, strlen(buf1));

close(fd);
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// dup2.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>


int main()
{
int fd = open("english.txt", O_RDWR);
if(fd == -1)
{
perror("open");
exit(1);
}

int fd1 = open("a.txt", O_RDWR);
if(fd1 == -1)
{
perror("open");
exit(1);
}

printf("fd = %d\n", fd);
printf("fd1 = %d\n", fd1);

int ret = dup2(fd1, fd);
if(ret == -1)
{
perror("dup2");
exit(1);
}
printf("current fd = %d\n", ret);
char* buf = "主要看气质 ^_^!!!!!!!!!!\n";
write(fd, buf, strlen(buf));
write(fd1, "hello, world!", 13);

close(fd);
close(fd1);
return 0;
}

线程共享文件

线程的定义:有时称轻量级进程,是进程中的一个执行线路或线索,是一个相对独立的、可独立调度和指派的执行单元

线程的创建:应用程序可以通过一个统一的clone()系统调用接口,用不同的参数指定创建轻量进程还是普通进程

clone()调用do_fork()创建线程, do_fork()参数为:(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND)

  • CLONE_VMdo_fork()需要调用copy_mm()来设置task_struct中的mm和active_mm项,这两个mm_struct数据与进程所关联的内存空间相对应。如果do_fork()时指定了CLONE_VM开关,copy_mm()将把新的task_struct中的mmactive_mm设置成与current的相同,同时提高该mm_struct的使用者数目(mm_struct::mm_users)。也就是说,轻量级进程与父进程共享内存地址空间
  • CLONE_FStask_struct中利用fs(struct fs_struct *)记录了进程所在文件系统的根目录和当前目录信息do_fork()时调用copy_fs()复制了这个结构;而对于轻量级进程则仅增加fs->count计数,与父进程共享相同的fs_struct。也就是说,轻量级进程没有独立的文件系统相关的信息,进程中任何一个线程改变当前目录、根目录等信息都将直接影响到其他线程
  • CLONE_FILES:一个进程可能打开了一些文件,在进程结构task_struct中利用files(struct files_struct *)来保存进程打开的文件结构(struct file)信息do_fork()中调用了copy_files()来处理这个进程属性;轻量级进程与父进程是共享该结构的,copy_files()时仅增加files->count计数。这一共享使得任何线程都能访问进程所维护的打开文件,对它们的操作会直接反映到进程中的其他线程
  • CLONE_SIGHAND:每一个Linux进程都可以自行定义对信号的处理方式,在task_struct中的sig(struct signal_struct)中使用一个struct k_sigaction结构的数组来保存这个配置信息,do_fork()中的copy_sighand()负责复制该信息;轻量级进程不进行复制,而仅仅增加signal_struct::count计数,与父进程共享该结构。也就是说,子进程与父进程的信号处理方式完全相同,而且可以相互更改

总结:线程间所有文件结构都为共享资源,不但“文件表项”(file对象)是共享的,就连“文件描述符表”(files_struct结构)也是共享的。线程的创建仅仅增加的是filesfs的引用计数,“文件打开计数”(file对象的引用计数)并没有增加,所以任何一个线程对打开的文件执行close操作,文件都将关闭(文件打开计数为1的情况)。但是如果线程不进行打开文件的关闭,则文件直到进程结束时才会关闭,这就是使用多线程实现tcp服务器时,服务线程必须要显示调用close的原因,否则永远不会发送FIN终止链接(因为主线程一直处于监听不会结束)。

进程间文件描述符的传递

  • 传递描述符的函数的参数是fd,fd是打开文件指针在数组中的下标
  • 将一个文件描述符传递给另一个进程后,文件的“访问计数”会增加
  • 进程间传递文件描述符可以看做跨进程的dup调用,也就是同一个file对象在不同进程间的映射
  • 对于网络接口返回的描述符 ,只能采取传递文件描述符的方法。
  • UNIX系统中两个方法:BSD sendmsgrecvmsg方法;SYSV ioctl方法
  • 进程间传递文件描述符时,发送进程和接收进程共享同一文件表项
  • 进程间文件描述符的传递,只是通过内核将接收文件的一个新file指针指向和发送进程的同一个file对象,并使这个file对象的引用计数增加

文件属性

文件类型

Linux系统中常见的文件类型

  • 普通文件 - :包含了某种形式的数据
  • 目录文件 d :包含了其他文件的名字以及指向与这些文件有关信息的指针
  • 字符特殊文件 c :提供对设备不带缓冲的访问
  • FIFO文件 p :用于进程间的通信,命名管道
  • 套接字文件 s :用于网络通信
  • 符号链接 l :使文件指向另一个文件

image-20220531105420782

文件权限

Linux系统通过进程的有效用户ID和有效用户组ID来决定进程对系统资源的访问权限

与一个进程相关联的用户ID和用户组ID有如下几种:

image-20220408120554130

通常情况下,有效用户ID等于实际用户ID,有效组ID等于实际组ID;

可执行文件的权限中有一个特殊标志,定义为“当执行此文件时,将进程的有效用户ID设置为文件的所有者”,与此类似,组ID也有类似的情况。

这两个标志位称为:“设置用户ID” 和 “ 设置组ID”, 这 两 位 都 包 含 在 stat信 息 中 的 st_mode 中 , 可 用S_ISUIDS_ISGID测试。

文件:

  • 对于一个文件的读权限决定了我们是否能够打开该文件进行读操作
  • 对一个文件的写权限决定了我们是否能够打开该文件进行写操作
  • 为了在open函数中对一个文件指定O_TRUNC标志,必须对该文件具有写权限
  • 执行某个可执行文件,都必须对该文件具有执行权限

目录:

  • 目录文件的执行权限也表示可以进入该目录
  • 通过文件名打开一个任意类型的文件时,对该文件路径名中包含的每一个目录都应具有执行权限
  • 为了在一个目录中创建一个新文件,必须对该目录具有写权限执行权限
  • 为了删除一个文件,必须对包含该文件的目录具有写权限执行权限,对该文件本身则不需要有读、写权限

文件权限检查

进程访问文件时,内核就进行文件权限检查。这种检查涉及到文件的所有者、文件的所有者所在组、进程有效用户、进程的有效组及进程的附加组。两个所有者是文件的性质,而有效用户与有效组是进程的性质

当进程对某个文件进行操作时,内核按顺序执行下列4步来检查文件权限

  • 若进程的有效用户为root(ID等于0),则允许任何操作;
  • 若进程的有效用户等于文件的所有者(ID相同)(即该进程拥有文件),按照文件所有者具有的权限判定操作是否合法
  • 若进程的有效组或进程的附加组之一等于文件所有者所在组,按照文件所有者所在组具有的权限判定操作是否合法
  • 按照其他用户具有权限判定操作是否合法

一般情况下:

  • 若进程有效用户拥有此文件,则按用户权限批准或拒绝该进程对文件的操作
  • 若进程有效用户并不拥有该文件,但进程有效用户属于某个适当的组,则按组权限批准或拒绝该进程对文件的操作
  • 若进程有效用户并不拥有该文件,也不属于某个适当的组,则按照其他其他用户权限批准或拒绝该进程对文件的操作

新文件和新目录的所有权:

新文件的所有者设置为进程的有效用户

新文件所有者所在的组,POSIX允许选择下列之一:

  • 新文件所有者所在的组可以是进程的有效组
  • 新文件所有者所在的组可以是它所在目录的组

新文件所有者所在的组取决于它所在目录的设置组ID位是否设置,若设置,则为目录组,否则则为进程有效组

BSD总是用目录组作为新文件所有者所在组

文件的时间

每个文件有三个时间字段

image-20220408123707548

对文件进行一次读操作,它的访问时间就会改变

修改时间是文件内容最后一次被修改的时间

状态时间是该文件索引节点最后一次被修改的时间,影响索引节点的操作:

  • 更改文件访问权限
  • 更改文件用户ID
  • 更改文件链接数

这三个时间在后文的stat结构体中也有展示

相关struct

文件和目录相关结构体

DIR结构体

mkdir函数的返回类型为DIR*closedirreaddir函数的传入参数类型为DIR*

1
2
3
4
5
6
7
8
9
10
11
struct __dirstream
{
void *__fd;
char *__data;
int __entry_data;
char *__ptr;
int __entry_ptr;
size_t __allocation;
size_t __size;
__libc_lock_define (, __lock)
}; typedef struct __dirstream DIR

dirent结构体

头文件所在位置:/usr/include/dirent.h

readdir函数的返回类型为struct dirent*

1
2
3
4
5
6
7
8
struct dirent
{
ino_t d_ino; // i节点号
off_t d_off; // 在目录文件中的偏移,相对目录开头而言
usigned short d_reclen; // 文件名长度
unsigned char d_type; // 文件类型
char d_name[256]; // 文件名
};

unsigned char d_type; // 文件类型

可以和下面的/usr/include/dirent.h)进行比较来判断文件的类型,如if(d_type == DT_DIR)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/* File types for `d_type'.  */
enum
{
DT_UNKNOWN = 0,
# define DT_UNKNOWN DT_UNKNOWN // 未知
DT_FIFO = 1,
# define DT_FIFO DT_FIFO // 管道
DT_CHR = 2,
# define DT_CHR DT_CHR // 字符设备
DT_DIR = 4,
# define DT_DIR DT_DIR // 目录
DT_BLK = 6,
# define DT_BLK DT_BLK // 块设备
DT_REG = 8,
# define DT_REG DT_REG // 普通文件
DT_LNK = 10,
# define DT_LNK DT_LNK // 软链接
DT_SOCK = 12,
# define DT_SOCK DT_SOCK // 套接字
DT_WHT = 14
# define DT_WHT DT_WHT
};

注:编译时添加宏定义 -D_BSD_SOURCE

stat结构

stat是linux系统用来描述文件属性的重要数据结构信息,可以使用stat命令查看。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct stat {
dev_t st_dev; //文件的设备编号
ino_t st_ino; //Inode节点号
mode_t st_mode; //文件的类型和存取的权限
nlink_t st_nlink; //文件的硬链接数,刚建立的文件值为1
uid_t st_uid; //用户ID
gid_t st_gid; //组ID
dev_t st_rdev; //(设备类型)若此文件为设备文件,则为其设备编号
off_t st_size; //文件大小(以字节为单位)
blksize_t st_blksize; //块大小(文件系统的I/O 缓冲区大小)
blkcnt_t st_blocks; //该文件所占的磁盘块数
time_t st_atime; //最后一次访问时间
time_t st_mtime; //最后一次修改时间
time_t st_ctime; //最后一次改变时间(指属性)
};

st_mode

mode_t st_mode 16位,无符号整数,其低16位定义如下:

image-20220530150108615

image-20220408113707390

注:下面的宏定义在/usr/include/linux/stat.h

1)从st_mode中获取低12bit信息

下面是一些掩码,是定义的宏。使用方法st_mode & 掩码9-11bit很少用)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| bit  | 字段               | 权限    | 含义                      |
| ---- | ----------------- | ------- | ------------------------ |
| 11 | S_ISUID | 0004000 | 设置用户ID |
| 10 | S_ISGID | 0002000 | 设置组ID |
| 9 | S_ISVTX | 0001000 | 黏住位 |
| 6-8 | S_IRWXU | 00700 | 过滤除所有者权限以外的信息 |
| 8 | S_IRUSR | 00400 | 所有者 - 读权限 |
| 7 | S_IWUSR | 00200 | 所有者 - 写权限 |
| 6 | S_IXUSR | 00100 | 所有者 - 执行权限 |
| 3-5 | S_IRWXG | 00070 | 过滤除所属组权限以外的信息 |
| 5 | S_IRGRP | 00040 | 用户组 - 读权限 |
| 4 | S_IWGRP | 00020 | 用户组 - 写权限 |
| 3 | S_IXGRP | 00010 | 用户组 - 执行权限 |
| 0-2 | S_IRWXO | 00007 | 过滤除其他人权限以外的信息 |
| 2 | S_IROTH | 00004 | 其他用户 - 可读权限 |
| 1 | S_IWOTH | 00002 | 其他用户 - 写权限 |
| 0 | S_IXOTH | 00001 | 其他用户 - 执行权限 |

2)文件类型信息(12-15bit)

使用:if(S_ISLNK(st_mode))

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#define S_IFMT  00170000    // 过滤st_mode中除文件类型以外的信息
#define S_IFSOCK 0140000 // 套接字
#define S_IFLNK 0120000 // 符号链接(软链接)
#define S_IFREG 0100000 // 普通文件
#define S_IFBLK 0060000 // 块设备
#define S_IFDIR 0040000 // 目录
#define S_IFCHR 0020000 // 字符设备
#define S_IFIFO 0010000 // 管道

#define S_ISLNK(m) (((m) & S_IFMT) == S_IFLNK) // 符号链接 l
#define S_ISREG(m) (((m) & S_IFMT) == S_IFREG) // 普通文件 -
#define S_ISDIR(m) (((m) & S_IFMT) == S_IFDIR) // 目录文件 d
#define S_ISCHR(m) (((m) & S_IFMT) == S_IFCHR) // 字符特殊文件 c
#define S_ISBLK(m) (((m) & S_IFMT) == S_IFBLK) // 块特殊文件 b
#define S_ISFIFO(m) (((m) & S_IFMT) == S_IFIFO) // 管道或FIFO p
#define S_ISSOCK(m) (((m) & S_IFMT) == S_IFSOCK) // 套接字 s

image-20220530145912738

设置用户ID位和设置组ID位

定义:

  • 包含在可执行文件的权限标记中,有一个“设置用户ID位”若该位被设置,表示:执行该文件时,进程的有效用户ID变为文件的所有者对于设置组ID位类似
  • 通过命令行设置用户ID位:
    • chmod u+s filenamechmod u-s filename
    • chmod g+s filenamechmod g-s filename
  • 若文件所有者是超级用户,且设置了设置用户ID位,则执行此文件的进程拥有超级用户权限
  • Passwd(1) 允许任一用户改变其口令,该程序是一个设置用户ID程序
  • 分别用常量S_ISUIDS_ISGID测试

粘着位

在UNIX早期版本中,有一位被称为粘住位,如果一可执行程序文件的这一位被设置了,那么在该程序第一次执行并结束时,该程序正文被保存在交换区中,这使得下次执行该程序时能较快地将其装入内存。

现今较新的UNIX系统大多数都具有虚存系统以及快速文件系统,所以不再需要使用这种技术

如果对一个目录设置了粘住位,则只有对该目录具有写许可权的用户并且满足下列条件之一,才能删除或更名该目录下文件:

  • 拥有此文件
  • 拥有此目录
  • 是超级用户

目录/tmp/var/spool/uucppublic(怎么没有?)是设置粘住位的候选者。这两个目录是任何用户都可在其中创建文件的目录,对任一用户(用户、组和其他)的许可权通常都是读、写和执行。但是用户不应能删除或更名属于其他人的文件,为此在这两个目录的文件方式中都设置了粘住位。

st_size

stat结构的成员st_size包含了以字节为单位的该文件的长度。此字段只对普通文件、目录文件和符号连接有意义

  • 对于普通文件,其文件长度可以是0,在读这种文件时,将得到文件结束指示
  • 对于目录,文件长度通常是一个数,例如16或512的整倍数
  • 对于符号连接,文件长度是在文件名中的实际字节数。例如:
    • lrwxrwxrwx 1 root 7 Sep 25 07:14 lib -> usr/lib
    • 其中,文件长度7就是路径名usr/lib的长度

st_dev

每一个文件系统所在的存储设备都由其主、次设备号表示。设备号所用的数据类型是基本系统数据类型dev_t

主设备号标识设备驱动程序,次设备号标识特定的子设备

通常可以使用两个宏majorminor来访问主、次设备号

  • 早期系统用16位整型存放设备号:8位用于主设备号,8位用于次设备号
  • FreeBSD 8.0和Mac OS X 10.6.8使用32位整型,其中8位表示主设备号,24位表示次设备号。
  • 32位系统中,Solaris 10用32位整型表示dev_t,其中14位用于主设备号,18位用于次设备
  • 64位系统中,Solaris 10用64位整型表示dev_t,主设备号和次设备号各为32位
  • 在Linux 3.2.0上,dev_t是64位整型,12位用于主设备号,20位用于次设备号

系统中与每个文件名关联的st_dev值是文件系统的设备号,该文件系统包含了这一文件名以及与其对应的索引结点

只有字符特殊文件和块特殊文件才有st_rdev,此值包含实际设备的设备号

passwd结构体

保存用户的信息

头文件所在/usr/include/pwd.hgetpwuid的返回值

1
2
3
4
5
6
7
8
9
struct passwd{
char * pw_name; /* 用户名*/
char * pw_passwd; /* 密码.*/
__uid_t pw_uid; /* 用户ID.*/
__gid_t pw_gid; /*组ID.*/
char * pw_gecos; /*真实名*/
char * pw_dir; /* 主目录.*/
char * pw_shell; /*使用的shell*/
};

group结构体

保存的用户组信息

头文件所在/usr/include/grp.hgetgrgid的返回值

1
2
3
4
5
6
7
struct group
{
char *gr_name; /*组名称*/
char *gr_passwd; /* 组密码*/
gid_t gr_gid; /*组ID*/
char **gr_mem; /*组成员账号*/
}

文件操作

获取文件的stat结构体:statfstatlstatfstatat

改变文件大小:truncateftruncate

获取用户的passwd结构体:getpwuid

获取用户组的group结构体:getgrgid

判断文件读,写,执行或是否存在:access

改变文件权限:chmod

改变文件属主属组:chown

创建硬链接:link

创建软链接:symlink

删除链接或文件:unlink

删除文件或目录:remove

文件重命名:rename

修改文件时间:utime

操纵文件描述符:fcntl

stat函数

man 2 stat

作用:读取文件属性

头文件:

  • #include <sys/types.h>
  • #include <sys/stat.h>
  • #include <unistd.h>

函数原型:

  • int stat(const char *path, struct stat *buf);
  • int lstat(const char *path, struct stat *buf);
  • int fstat(int fd, struct stat *buf);
  • int fstatat(int fd, const char *restrict pathname, struct stat *restrict buf, int flag);

函数说明:

  • statlstat参数相同,功能类似。读取path参数所指定文件的文件属性并将其填充到buf参数所指向的结构体中;对于符号链接文件,lstat返回符号链接的文件属性,stat返回符号链接引用文件的文件属性

    • stat 穿透(追踪)函数 — 软连接
    • lstat 不穿透,不穿透的Linux命令有ls -lrm
  • fstat与前两个函数功能类似,指定文件的方式改为通过文件描述符

  • statat函数为一个相对于当前打开目录(由fd参数指向)的路径名返回文件统计信息。 flag参数控制着是否跟随着一个符号链接,返回符号链接本身的信息或者符号链接所指向的实际文件信息。
    • 如果fd参数的值是AT_FDCWD,并且pathname参数是一个相对路径名,fstatat会计算相对于当前目录的pathname参数,如果pathname是一个绝对路径,fd参数就会被忽略。这两种情况下,根据flag的取值fstatat的作用就跟statlstat一样
  • 参数buf,是一个指向stat结构的指针,这几个函数都是为了填充buf指向的结构stat
  • 成功返回0,失败返回-1

truncate函数

man 2 truncate

作用:改变文件大小

函数原型:

  • int truncate(const char* pathname, off_t length);
  • int ftruncate(int fd, off_t length);

函数说明:

  • 用于改变文件的长度
  • pathname:欲改变长度的文件的文件名
  • fd:欲改变长度的文件的文件描述符;
  • length:要设置的文件的新长度

返回值:成功返回0,出错返回-1

注意事项:

  • 当文件以前的长度>length时,则超过length以外的数据将不复存在
  • 当文件以前的长度<length时,在文件以前长度到length之间,将形成空洞,读该区域,将返回0

常见问题:

truncateftruncate函数并未实质性的向磁盘写入数据,只是分配了一定的空间供当前文件使用。当fd<length时,此时如果使用十六进制编辑工具打开该文件,会发现文件末尾多了很多00,这就是执行这个函数后的效果。如果发生系统复位或者装置掉电以后,该函数所产生的作用将被文件系统忽略,也就是说它所分配的空间将不能被识别,文件的大小将会是最后一次写入操作的区域大小,而非ftruncate分配的空间大小,也就是说,文件大小有可能会被改变

解决方案:

可以在执行完ftruncate之后,在新空间的末尾写入一个或以上字节的数据(不为0x00),这样新空间则不为空,文件系统会把这部分空间当成这个文件的私有空间处理,而不会出现文件大小改变的错误。

getpwuid

man 3 getpwuid

作用:获取用户信息

常用函数:getpwuid

头文件:

  • sys/types.h
  • pwd.h

函数原型:struct passwd *getpwuid(uid_t uid);

函数说明:输入用户ID,返回用户属性信息(passwd结构)

getgrgid

man 3 getgrgid

作用:获取用户组信息

头文件:

  • sys/types.h
  • grp.h

函数原型:struct group *getgrgid(gid_t gid);

函数说明:输入用户组ID,返回用户组属性信息(group结构)

access函数

man 2 access

函数功能:按照前述文件权限检查的4个步骤测试存取文件是否具有相应权限

函数原型:int access(const char *pathname, int mode);

成功返回0,失败返回-1

Mode 说明
R_OK 测试读许可
W_OK 测试写许可
X_OK 测试执行许可
F_OK 文件是否存在
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main(int argc, char *argv[])
{
if(argc < 2)
{
printf("a.out filename\n");
exit(1);
}

int ret = access(argv[1], W_OK);
if(ret == -1)
{
perror("access");
exit(1);
}
printf("you can write this file.\n");
return 0;
}

chmod函数

man 2 chmod

作用:改变文件权限

函数原型

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

函数用途:改变指定文件的权限位。

函数说明:

  • chmod要求给出的是文件或目录所在的位置,而fchmod主要针对的是文件,要求调用是相应的文件描述符。
  • 修改时,进程的有效用户ID必须等于文件的所有者ID,或是root运行的此进程
  • 返回值:成功0,失败-1

mode 必须是8进制数,如0755

chmod出错信息

字段 说明
EACCES 给出的文件所处路径没有访问权限
EFAULT 路径指向的文件地址错误
EIO 发生I/O错误
ELOOP 给出的文件所在路径中符号连接过多
ENAMETOOLONG 路径过长
ENOENT 文件不存在
ENOMEM 内核内存空间不足
ENOTDIR 给出的文件所处路径中包含不是目录的部分
EPERM 有效用户ID与文件拥有者不同,进程无权访问修改文件权限
EROFS 文件位于只读文件系统

fchmod函数出错信息

字段 说明
EBADF 非法的文件描述符
EIO 发生I/O错误
EPERM 有效用户ID与文件拥有者不同,进程无权访问修改文件权限
EROFS 文件位于只读系统
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>

int main(int argc, char *argv[])
{
if(argc < 2)
{
printf("a.out filename\n");
exit(1);
}

int ret = chmod(argv[1], 0775);
if(ret == -1)
{
perror("chmod");
exit(1);
}
return 0;
}

chown函数

man 2 chown

作用:更改文件所有者

函数原型

  • int chown( const char *pathname, uid_t owner, gid_t group);
  • int fchown( int filedes, uid_t owner, gid_t group);
  • int lchown( const char *pathname, uid_t owner, gid_t group);

函数用途:更改文件的用户ID和组ID。

函数说明:

  • 如果两个参数ownergroup中的任意一个是-1,则不改变文件所有者或文件所属用户组
  • lchown是改变符号链接本身的所有者,而不是该符号链接所指向的文件
  • 基于BSD的系统中,只有超级用户才能更改一个文件的所有者
  • 非超级用户进程调用,则在成功返回时,该文件的设置用户ID位和设置组ID位会被清除
  • 成功返回0,若出错则返回-1

link函数

man 2 link

作用:创建一个硬链接

函数原型:int link ( const char *pathname, const char *newpath);

函数用途:任何一个文件可以有多个目录项(dentry)指向其索引节点,创建一个指向现有文件的新目录项

函数说明

  • 此函数创建一个新目录项newpath,它指向pathname指向的文件。如果newpath已经存在,则返回出错
  • 创建新目录项以及增加连接计数是一个原子操作。
  • 大多数的系统规定,只有超级用户可以创建指向一个目录的新连接,目的是避免在文件系统中形成循环。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//link.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main(int argc, char* argv[])
{
if(argc < 3)
{
printf("a.out oldpath newpath\n");
exit(0);
}

int ret = link(argv[1], argv[2]);
if(ret == -1)
{
perror("link");
exit(1);
}
return 0;
}
// 示例,为test.txt文件创建硬链接
// ./link test.txt test.link

symlink函数

man 2 symlink

作用:创建一个软链接

头文件:#include <unistd.h>

函数原型:int symlink(const char *target, const char *linkpath);

  • target是原文件路径
  • linkpath是要创建的软链接
  • 返回值:成功返回0,失败返回-1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//symlink.c
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

int main(int argc, char* argv[])
{
if(argc < 3)
{
printf("a.out oldpath newpath\n");
exit(1);
}

int ret = symlink(argv[1], argv[2]);
if(ret == -1)
{
perror("symlink");
exit(1);
}

return 0;
}
// 示例,为test.txt文件创建软链接
// ./link test.txt test.slink

unlink函数

man 2 unlink

函数原型:int unlink ( const char *pathname);

头文件:unistd.h

函数用途:unlink删除目录项,并将由pathname所引用文件的链接计数减1,清空这个文件使用的可用的系统资源

  • 删除一个文件的目录项并减少它的链接数,若成功则返回0,否则返回-1,错误原因存于errno
  • 如果想通过调用这个函数来成功删除文件,你就必须拥有这个文件的所属目录的写和执行权限

注:

1)如果是符号链接,删除符号链接

2)如果是硬链接,硬链接数减1,当减为0时,释放数据块和inode

3)如果文件硬链接数为0,但有进程已打开该文件,并持有文件描述符,则等该进程关闭该文件时,kernel才真正去删除该文件

  • 利用该特性创建临时文件,先open或creat创建一个文件,马上unlink此文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include <stdlib.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>

int main()
{
int fd = open("tmpfile", O_CREAT | ORDWR, 0664);
if(fd == -1)
{
perror("open");
exit(1);
}
//删除临时的file
int ret = unlink("tmpfile");
//向临时文件写入数据
write(fd, "hello\n", 5);

//重置文件指针
lseek(fd, 0, SEEK_SET);
//读取临时文件数据
char buf[24] = {0};
int len = read(fd, buf, sizeof(buf));
//写到屏幕上
write(1, buf, len);
//关闭临时文件
close(fd);
fd = open("tmpfile", O_RDONLY);
if(fd == -1)
{
perror(NULL);
exit(1);
}
return 0;
}

remove函数

man 3 remove

函数原型:int remove( const char *pathname);

头文件:stdio.h

函数用途:remove删除一个目录中的一个或多个文件或目录,也可以将某个目录及其下的所有文件及子目录均删除

remove的参数为普通文件时等价于unlink

rename函数

man 2 rename

函数原型:int rename ( const char * oldname, const char * newname);

函数用途:更名文件或目录

函数说明

  • 如果oldname是一个文件而不是目录,那么为该文件更名
  • 如果oldname是一个目录,那么为该目录更名
  • 如果oldnamenewname引用同一文件,则函数不做任何更改而成功返回
  • 应对包含两个文件的目录具有写和执行许可权

utime函数

man 2 utime

函数原型:int utime( const char *pathname, const struct utimbuf *times);

函数用途:设置存取和修改文件的时间

函数说明:此函数的操作以及执行它所要求的优先权取决于times参数是否是NULL

  • 如果times是一个NULL,则存取时间和修改时间两者都设置为当前时间,但必须满足下面二者之一:
    • 进程的有效用户ID必须等于该文件的所有者
    • 进程对该文件有写许可权
  • 如果times是非空指针,则存取时间和修改时间被设置为times所指向的时间,此时进程的有效用户ID必须等于该文件的所有者ID,或者进程必须是超级用户的进程

fcntl函数

man 2 fcntl

作用:根据文件描述符来操作文件的状态

头文件:#include <fcntl.h>

函数原型:fcntl可变参的,第3个参数是否需要取决于cmd

  • int fcntl(int fd, int cmd);
  • int fcntl(int fd, int cmd, long arg);
  • int fcntl(int fd, int cmd, struc flock *lock);

参数:cmd 宏定义

功能:宏后面为第三个参数类型

1)复制一个现有的文件描述符 — cmdF_DUPFD(int)

2)获得/设置文件描述符标记 — cmdF_GETFD(void) / F_SETFD(int)

3)获得 / 设置文件状态标记 — cmdF_GETFL(void) / F_SETFL(int)

1
2
3
4
5
6
7
8
9
10
11
12
F_GETFL
O_RDONLY // 只读打开
O_WRONLY // 只写打开
O_RDWR // 读写打开
O_EXEC // 执行打开
O_SEARCH // 搜索打开目录
O_APPEND // 追加写
O_NONBLOCK // 非阻塞模式

F_SETFL
O_APPEND
O_NONBLOCK

4)获得 / 设置异步I/O所有权 — cmdF_GETOWN / F_SETOWN

5)获得 / 设置记录锁 — cmdF_GETLK / F_SETLK / F_SETLKW

fcntl使用示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>


int main(void)
{
int fd;
int flag;

// 测试字符串
char *p = "我们是一个有中国特色的社会主义国家!!!!!!";
char *q = "呵呵, 社会主义好哇。。。。。。";

// 只写的方式打开文件
fd = open("test.txt", O_WRONLY);
if(fd == -1)
{
perror("open");
exit(1);
}

// 输入新的内容,该部分会覆盖原来旧的内容
if(write(fd, p, strlen(p)) == -1)
{
perror("write");
exit(1);
}

// 使用 F_GETFL 命令得到文件状态标志
flag = fcntl(fd, F_GETFL, 0);
if(flag == -1)
{
perror("fcntl");
exit(1);
}

// 将文件状态标志添加 ”追加写“ 选项
flag |= O_APPEND;
// 将文件状态修改为追加写
if(fcntl(fd, F_SETFL, flag) == -1)
{
perror("fcntl -- append write");
exit(1);
}

// 再次输入新内容,该内容会追加到旧内容的后面
if(write(fd, q, strlen(q)) == -1)
{
perror("write again");
exit(1);
}

// 关闭文件
close(fd);

return 0;
}

目录操作

获取当前目录:getcwdget_current_dir_name

打开目录:opendir

关闭目录:closedir

创建目录:mkdir

删除目录:rmdir

改变当前工作目录:chdir

读取目录下的文件:readdir

重置目录读取位置:rewinddir

设置目录读取位置:seekdir

获取目录读取位置:telldir

读取特定目录数据:scandir

getcwd函数

man 3 getcwd

作用:获取当前目录

头文件: unistd.h

函数声明:char *getcwd(char *buf, size_t size);

函数说明:将当前的工作目录绝对路径字符串复制到参数buf所指的缓冲区,参数sizebuf缓冲区大小。

返回值:成功调用返回指向buf的指针,失败返回NULL

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>
#include <unistd.h>
#define MAX_SIZE 255

int main()
{
char path[MAX_SIZE];
getcwd(path, sizeof(path));
puts(path);
return 0;
}
1
2
3
yang@Ubuntu18:~/Documents/Cprogram/$ gcc getcwd_test.c -o getcwd_test
yang@Ubuntu18:~/Documents/Cprogram/$ ./getcwd_test
/home/yang/Documents/Cprogram

get_current_dir_name函数

man 3 get_current_dir_name

作用:获取当前目录

文件首加宏定义:#define _GNU_SOURCE

头文件: unistd.h

函数声明:char * get_current_dir_name(void);

函数说明:调用后会返回一个字符串指针,指向当前工作目录绝对路径字符串

返回值:成功则返回字符串指针,失败则返回NULL,错误代码存放于errno

1
2
3
4
5
6
7
8
9
10
11
#define _GNU_SOURCE
#include <stdio.h>
#include <unistd.h>

int main()
{
char *path;
path = get_current_dir_name();
puts(path);
return 0;
}
1
2
3
yang@Ubuntu18:~/Documents/Cprogram$ gcc get_current_dir_name_test.c -o get_current_dir_name_test
yang@Ubuntu18:~/Documents/Cprogram$ ./get_current_dir_name_test
/home/yang/Documents/Cprogram

opendir函数

man 3 opendir

作用:打开目录

头文件:

  • sys/types.h
  • dirent.h

函数声明:DIR *opendir(const char *path);

函数说明:打开参数path指定的目录

返回值:成功则返回DIR形态的目录流,失败则返回NULL,错误代码存放于errno

错误代码:

  • EACCESS 代表权限不足,也就是对这个目录没有执行权限。
  • EMFILE 表示目前同时打开这个文件的进程数目已经到达了系统上限。
  • ENFILE 代表已经达到系统可以同时打开的文件数上限。
  • ENOTDIR 代表参数path指向的不是一个真正的目录。
  • EOENT 表示参数path指向的目录不存在,或者参数path是一个空的字符串。
  • ENOMEM 表示核心内存不足

closedir函数

man 3 closedir

作用:关闭目录

头文件:

  • sys/types.h
  • dirent.h

函数声明:int closedir(DIR *dir);

函数说明:关闭参数dir指定的目录

返回值:关闭成功就返回0,失败的话就返回-1,错误原因存于errno

错误代码:EBADF代表参数dir是一个无效的目录流

readdir函数

man 2 readdir

作用:读取目录文件

头文件:

  • sys/types.h
  • dirent.h

函数声明:struct dirent *readdir(DIR *dirp);

函数说明:读取目标流dir标识的目录

返回值:每执行一次readdir,这个函数返回指向当前读取目录项结构的指针,有错误发生或读取到目录文件尾的时候就返回NULL

错误代码:EBADF代表参数dir是一个无效的目录流

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#define _GNU_SOURCE
#include <sys/types.h>
#include <dirent.h>
#include <stdio.h>
#include <unistd.h>

#define MAX_SIZE 255
#define path "/home/yang/Documents/Cprogram"

int main()
{
DIR *dir = opendir(path);
char dir1[MAX_SIZE];
getcwd(dir1, sizeof(dir1));
puts(dir1);
char *dir2 = get_current_dir_name();
puts(dir2);
struct dirent *dent = NULL;
while((dent = readdir(dir)) != NULL)
{
puts(dent->d_name);
}
return 0;
}

image-20220407155354291

获取一个目录下面普通文件个数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <dirent.h>

int get_file_count(char* root)
{
DIR* dir;
struct dirent* ptr = NULL;
int total = 0;
char path[1024];

dir = opendir(root);
if(dir == NULL)
{
perror("opendir");
exit(1);
}

while((ptr = readdir(dir)) != NULL)
{
if(strcmp(ptr->d_name, ".") == 0 || strcmp(ptr->d_name, "..") == 0)
{
continue;
}
if(ptr->d_type == DT_DIR)
{
sprintf(path, "%s/%s", root, ptr->d_name);
total += get_file_count(path);
}
if(ptr->d_type == DT_REG)
{
total ++;
}
}
closedir(dir);

return total;
}

int main(int argc, char* argv[])
{
if(argc < 2)
{
printf("a.out path\n");
exit(1);
}

int total = get_file_count(argv[1]);
printf("%s has %d files!\n", argv[1], total);

return 0;
}

mkdir函数

man 2 mkdir

作用:创建目录

头文件:sys/stat.h

函数声明:int mkdir(const char *pathname, mode_t mode);

函数说明:以mode方式创建一个以参数pathname命名的目录,mode定义新创建目录的权限

返回值:若目录创建成功,则返回0;否则返回-1,并将错误记录到全局变量errno

注意事项:

  • 创建777权限的目录,需要先执行umask(0),然后再调用mkdir函数
  • 目录已存在会返回-1
  • 不能创建多个目录
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <stdio.h>
#include <sys/stat.h>
#define PATH "home/yang/Documents/Cprogram/Linux系统编程学习/mulu"

int main()
{
umask(0); //不调用umask,最高权限775
if(mkdir("目录", S_IRWXU | S_IRWXG | S_IRWXO) == -1) //权限777
{
printf("mkdir error\n");
}//在当前下创建

if(mkdir("../mulu", S_IRWXU | S_IRGRP | S_IROTH) == -1)
{
printf("mkdir error\n");
}//在上级目录下创建

if(mkdir("../Linux系统编程学习/mulu", S_IRWXU | S_IRGRP) == -1)
{
printf("mkdir error\n");
} //这是ok的,在当前目录上一级目录下的 Linux系统编程学习 文件夹下创建 mulu

if(mkdir(PATH, S_IRWXU ) == -1)
{
printf("mkdir error\n");
} //使用绝对路径创建

return 0;
}

rmdir函数

man 2 rmdir

作用:删除空目录

头文件:unistd.h

函数声明:int rmdir(const char *pathname);

函数说明:删除一个目录,该目录必须是空的

返回值:若目录删除成功,则返回0;否则返回-1,并将错误记录到全局变量errno

注意事项:pathname不能超过255,目录名不能以.开头(.开头的是隐藏文件)。目录没有被其他进程占用。

1
2
3
4
5
6
7
8
9
10
11
#include <unistd.h>
#include <stdio.h>

int main()
{
if(rmdir("./mulu") == -1)
{
printf("rmdir error\n");
}
return 0;
}

chdir函数

man 2 chdir

作用:改变当前工作目录

头文件:unistd.h

函数声明:int chdir(const char *path);

函数说明:chdir函数用于改变当前工作目录。调用参数是指向目录的指针,调用进程需要有搜索整个目录的权限

返回值:成功,则返回0;否则返回-1,并将错误记录到全局变量errno

错误信息:

  • EFAULT: path指向了非法地址
  • ENAMETOOLNG: 路径过长
  • ENOENT: 文件不存在
  • ENOMEM: 内核内存不足
  • ENOTDIR: 给出路径不是目录
  • EACCES: 无访问路径中某个目录的权限
  • ELOOP:解析路径中太多的符号链接
  • EIO: 发生I/O错误

函数说明:

  • 每个进程都具有一个当前工作目录。在解析相对目录引用时,该目录是搜索路径的开始之处
  • 如果调用进程更改了目录,则它只对该进程有效,而不能影响调用它的那个进程
  • 在退出程序时,shell还会返回开始时的那个工作目录
  • 内核解析参数中的路径名,并确保这个路径名有效。内核定位该目录的索引节点,并检查它的文件类型和权限位,确保目标文件是目录以及进程的所有者可以访问该目录
  • 内核用新目标目录的路径名和索引节点替换u区中当前目录路径名和索引节点号
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#define _GNU_SOURCE
#include <unistd.h>
#include <stdio.h>
#define PATH "/home/yang"

int main()
{
if(chdir("..") == -1)
{
puts("chdir error");
} //相对路径
char *nowdir = get_current_dir_name();
puts(nowdir);

if(chdir(PATH) == -1)
{
puts("chdir error");
} //绝对路径
nowdir = get_current_dir_name();
puts(nowdir);

return 0;
}

image-20220407174200065

rewinddir函数

man 3 rewinddir

作用:重置目录读取位置

头文件:

  • sys/types.h
  • dirent.h

函数声明:void rewinddir(DIR *dir);

函数说明:设置参数dir指向的目录流目前的读取位置为原来开头的读取位置

返回值:无返回值,函数执行失败后会将错误记录到全局变量errno

错误信息:EBADF:表示dir指向的目录流无效

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <stdio.h>
#include <dirent.h>
#include <sys/types.h>

int main()
{
DIR *dir = opendir("./"); //读取当前目录
struct dirent *dent = NULL;
//先读3个文件
dent = readdir(dir);
puts(dent->d_name);
dent = readdir(dir);
puts(dent->d_name);
dent = readdir(dir);
puts(dent->d_name);
//设置会开头
rewinddir(dir);
//在读3个文件
dent = readdir(dir);
puts(dent->d_name);
dent = readdir(dir);
puts(dent->d_name);
dent = readdir(dir);
puts(dent->d_name);

return 0;
}

image-20220407174829214

seekdir函数

man 3 seekdir

作用:设置目录的读取位置

头文件: dirent.h

函数声明:void seekdir(DIR * dir, off_t offset)

函数说明:设置参数dir指向的目录流目前的读取位置为offset,在调用readdir()时便从此新位置开始读取。参数offset代表距离目录文件开头的偏移量

返回值:无返回值,函数执行失败后会将错误记录到全局变量errno

错误信息:EBADF:表示dir指向的目录流无效

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include <stdio.h>
#include <dirent.h>
#include <sys/types.h>

int main()
{
DIR *dir = opendir("./");
struct dirent *dent = NULL;

//先读3个文件
dent = readdir(dir);
puts(dent->d_name);
printf("该文件的offset = %ld\n\n", dent->d_off);

dent = readdir(dir);
puts(dent->d_name);
printf("该文件offset = %ld\n\n", dent->d_off);

off_t offset = dent->d_off; //文件的偏移量是相对目录开头而言的,不要叠加

dent = readdir(dir);
puts(dent->d_name);
printf("该文件offset = %ld\n\n", dent->d_off);

seekdir(dir, offset);
printf("移动到离目录开头偏移%ld\n\n", offset);

//在读3个文件
dent = readdir(dir);
puts(dent->d_name);

dent = readdir(dir);
puts(dent->d_name);

dent = readdir(dir);
puts(dent->d_name);

return 0;
}

image-20220407180857330

telldir函数

man 3 telldir

作用:获取当前目录读取位置

头文件: dirent.h

函数声明:off_t telldir(DIR * dir);

函数说明:取得目录流dir的读取位置。

返回值:返回参数dir目录流目前的读取位置. 此返回值代表距离目录文件开头的偏移量,有错误发生时返回-1.

错误信息:EBADF:表示dir指向的目录流无效

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#define _GNU_SOURCE
#include <sys/types.h>
#include <dirent.h>
#include <stdio.h>
#include <unistd.h>

int main()
{
DIR *dir = opendir("./");
struct dirent *dent = NULL;
int i = 0;
while(i++ < 5)
{
dent = readdir(dir);
puts(dent->d_name);
printf("dirent->d_off = %ld\n", dent->d_off);
off_t offset = telldir(dir);
printf("telldir = %ld\n\n", offset);
}
return 0;
}

image-20220407181940228

scandir函数

man 3 scandir

作用:读取特定目录数据

头文件: dirent.h

函数声明:

1
2
3
4
5
int scandir(const char *dir, 
struct dirent ***namelist,
int (*filter) (const struct dirent*),
int(*compar)(const struct dirent **, const struct dirent**)
);

函数说明:scandir()会扫描参数dir指定的目录文件,经由参数filter指定的函数来挑选目录结构至参数namelist数组中,最后再调用参数compar指定的函数来排序namelist数组中的目录数据。每次从目录文件中读取一个目录结构后便将此结构传给参数filter所指的函数,filter函数若不想要将此目录结构复制到namelist数组就返回0,若filter为空指针则代表选择所有的目录结构。scandir()会调用qsort()来排序数据,参数compar则为qsort()的参数,若是要排列目录名称字母则可使用alphasort()

返回值:成功则返回复制到namelist数组中的数据结构数目,有错误发生则返回-1.

错误信息:ENOMEM 核心内存不足

补充:

  • int alphasort(const void **a, const void **b);
  • int versionsort(const void **a, const void **b);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <dirent.h>
#include <malloc.h>

int main()
{
struct dirent **namelist;
int n;

n = scandir(".", &namelist, 0, alphasort);
if (n < 0)
{
perror("not found\n");
}
else
{
while(n--)
{
printf("%s\n", namelist[n]->d_name);
free(namelist[n]);
}
free(namelist);
}
return 0;
}

补充

errno

errno是一个全局变量,是一个int值,是记录系统的最后一次错误代码。定义在头文件 errno.h 中,位置/usr/include/errno.h

任何标准C库函数都能对其进行修改(Linux系统函数更可以)

错误宏定义位置:

  • 第 1 - 34 个错误定义:/usr/include/asm-generic/errno-base.h

    image-20220527195432729

  • 第 35 - 133 个错误定义:/usr/include/asm-generic/errno.h

    image-20220527195552372

每个errno值对应着以字符串表示的错误类型,当调用”某些”函数出错时,该函数会重新设置 errno 的值

输出errno对应的错误信息:

1)使用 strerror函数,头文件string.h

  • 原型:char *strerror(int errnum);
  • 根据传入的errno返回对应的字符串

2)使用 perror函数,头文件stdio.h

  • 原型:void perror(const char *s);
  • 用来将上一个函数发生错误的原因输出到标准设备(stderr)
  • 参数 s 所指的字符串会先打印出,后面再加上错误原因字符串
  • 此错误原因依照全局变量errno的值来决定要输出的字符串。

umask

open函数创建文件或mkdir函数创建目录,在设置权限时,如果设置0777,创建的文件或目录却不是rwxrwxrwx的权限

原因:Linux有一个文件创建掩码umaskumask按位取反得到maskmask0777按位与得到文件或目录的权限

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/* myopen.c */
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

int main()
{
int fd = open("hello.c", O_RDWR|O_CREAT, 0777); //创建777权限的hello.c文件
if(fd == -1)
{
perror("open file");
exit(1);
}
int ret = close(fd);
if(ret == -1)
{
perror("file close");
exit(1);
}
return 0;
}

image-20220527203506121

得到的权限是755

umask查看文件创建掩码

image-20220527203604179

解决方法:将文件创建掩码

1)在Linux终端输入umask 0 ,将文件创建掩码临时设为0

2)使用mode_t umask(mode_t mask);函数,在代码中创建文件之前,添加umask(0);

链接穿透

image-20220530180258035

backspace

C语言程序从终端输入Backspace键显示^H

在程序中添加system("stty erase ^H");

欢迎关注我的其它发布渠道