UNIX环境编程-文件系统(9)
UNIX环境编程-文件系统(9)
一、目录和文件
1.获取文件属性
(1)stat、fstat、lstat
在 C 语言中,stat
,fstat
和 lstat
函数用于获取文件的状态信息。这些信息包括文件大小,创建和修改时间,权限等。这些函数在 <sys/stat.h>
头文件中定义
1 |
stat
函数获取指定路径的文件的状态信息
1 | int stat(const char *restrict pathname, struct stat *restrict statbuf); |
fstat
函数获取一个已经打开的文件(由文件描述符指定)的状态信息
1 | int fstat(int fd, struct stat *statbuf); |
lstat
函数类似于 stat
,但当指定的路径是一个符号链接时,lstat
返回的是这个链接本身的信息,而 stat
返回的是链接指向的文件的信息:
1 | int lstat(const char *restrict pathname, struct stat *restrict statbuf); |
所有这三个函数都把信息保存在一个 struct stat
结构体中。这个结构体包含了很多字段,包括:
st_mode
:文件类型和权限st_ino
:节点编号st_dev
:包含该文件的设备编号st_nlink
:硬链接的数量st_uid
:文件的用户 IDst_gid
:文件的组 IDst_size
:文件的大小(字节)st_atime
,st_mtime
,st_ctime
:文件的访问,修改和状态改变的时间st_blksize
:优化读文件的块大小st_blocks
:文件占用的磁盘块数
例如,下面的程序获取一个文件的大小
1 |
|
filesize.c
1 |
|
在终端中使用 下面的命令查看文件属性
1 | $stat filesize.c |
其中大小为:404
运行程序的可执行文件
1 | $ ./filesize filesize.c |
2.文件访问权限
st_mode
:stat结构体中st_mode元素涵盖了文件的权限信息,是一个16位的位图,用于表示文件类型,文件访问权限,以及特殊权限位。
st_mode
是在 C 语言的 stat
结构中用于描述文件类型和文件权限的成员。stat
结构是 stat()
, fstat()
, lstat()
等函数的输出,这些函数用来获取文件的属性。
st_mode
是一个位字段,其中一部分用于指示文件类型,另一部分用于指示文件权限。
在 <sys/stat.h>
头文件中定义了一系列的宏,可以用来检测 st_mode
中的不同位。
例如,这些宏可以用来检测文件类型:
S_ISREG(m)
: 如果 m 是一个普通文件,则返回非零值。S_ISDIR(m)
: 如果 m 是一个目录,则返回非零值。S_ISCHR(m)
: 如果 m 是一个字符设备,则返回非零值。S_ISBLK(m)
: 如果 m 是一个块设备,则返回非零值。S_ISFIFO(m)
: 如果 m 是一个FIFO或者管道,则返回非零值。S_ISLNK(m)
: 如果 m 是一个符号链接,则返回非零值。S_ISSOCK(m)
: 如果 m 是一个套接字,则返回非零值。
下面的代码检查一个文件是否是一个目录:
1 |
|
3.umask
(1)-rw-rw-r--
的含义
在Linux中,文件权限通常由一个字符串表示,例如-rw-rw-r--
。这个字符串由10个字符组成,每个字符的含义如下:
第一个字符表示文件类型:
-
表示普通文件
d
表示目录l
表示符号链接- 其他字符表示其他特殊类型的文件
接下来的三个字符表示文件所有者的权限:
r
表示读权限w
表示写权限x
表示执行权限-
表示没有对应的权限
因此,在-rw-rw-r--
中,rw-
表示文件所有者具有读和写权限。
- 然后的三个字符表示文件所属的用户组的权限,它们的含义与文件所有者的权限相同。
- 最后的三个字符表示其他用户的权限,它们的含义也与文件所有者的权限相同。
因此,在-rw-rw-r--
中,rw-rw-r--
表示文件所有者和文件所属的用户组具有读写权限,其他用户只有读权限
(2)umask的使用
防止产生权限过松的文件
umask
是Unix和Linux系统中用于设置新文件默认权限的命令和函数。它用于控制新建文件或者目录的默认权限设置。具体的权限位是通过取umask
和默认权限位(通常为666对于文件,或者777对于目录)的反向位与运算来得到。
举个例子,如果umask
值被设置为022,新建文件的默认权限就会是644(也就是rw-r–r–)。这是因为默认权限666(也就是rw-rw-rw-)和umask值022进行反向位与运算的结果是644。
同样的,新建目录的默认权限会是755(也就是rwxr-xr-x)。这是因为默认权限777(也就是rwxrwxrwx)和umask值022进行反向位与运算的结果是755
可以在命令行中用umask
命令来查看或者修改当前的umask
值。例如,输入umask
并按回车,就会显示当前的umask
值。输入umask 022
并回车,就会设置umask
值为022
需要注意的是,umask
不应该被用于修改已经存在的文件或者目录的权限,对于这个目的应该使用chmod
命令
在终端可以输入umask
查看当前新建文件的权限
1 | (base) zxz@zxz-B660M-GAMING-X-AX-DDR4:~/Proj/CLionProj/UNIX/IO/fs$ umask |
终端输入ll
查看文件
1 | (base) zxz@zxz-B660M-GAMING-X-AX-DDR4:~/Proj/CLionProj/UNIX/IO/fs$ ll |
更改权限
1 | $umask 0666 |
则此时新创建的文件权限为0666 & ~0666
= 0000
若创建的是目录则文件权限为0777 & ~0666
创建新文件big1.c
查看文件权限
1 | (base) zxz@zxz-B660M-GAMING-X-AX-DDR4:~/Proj/CLionProj/UNIX/IO/fs$ touch big1.c |
可以知道big1.c
文件权限为----------
终端的umask
指令是通过系统调用umask进行封装的,可以通过
1 | $ man 2 umask # 查看umask系统调用函数 |
4.文件权限的更改/管理
(1)chmod
可以改变文件的权限
如:chmod a+x big.c
a
代表所有用户,包括文件所有者、用户组和其他用户。它是 “all” 的缩写。也可以使用u
代表文件所有者 (“user”),g
代表用户组 (“group”),o
代表其他用户 (“others”)+
是一个操作符,表示添加权限x
表示执行权限chmod a+x big.c
这个命令的意思是,给所有用户添加对big.c
文件的执行权限。在执行了这个命令之后,所有用户都可以执行big.c
文件(如果它是一个可执行的程序)
在程序编写中可以使用chmod()
与fchmod()
函数对文件的的权限进行更改
1 |
|
5.粘住位
粘住位(Sticky Bit)是 Unix 和 Unix-like 系统中的一个权限标志,通常用于目录权限的设置
目的:
粘住位的主要作用是防止非所有者删除或重命名目录下的文件。在默认情况下,如果用户对一个目录有写权限和执行权限,那么他可以删除目录下的任何文件,不管这个文件是由哪个用户创建的。这在某些公共目录(比如/tmp和/var/tmp)中可能会造成问题。通过设置粘住位,我们可以改变这种情况。当一个目录的粘住位被设置之后,只有文件的所有者或者超级用户(root)才能删除或重命名目录下的文件。
在 Unix-like 系统中,你可以使用 chmod 命令来设置粘住位。例如,以下命令会在 /tmp 目录上设置粘住位
1 | $ chmod +t /tmp |
如果你查看一个设置了粘住位的目录的权限,你会发现权限的最后一位是 t,比如:
1 | drwxrwxrwt 30 root root 4096 Apr 19 19:16 /tmp |
6.文件系统:FAT
、UFS
文件系统:文件或者数据的存储与管理
(1)inode
在 Unix 或 Unix-like 系统(比如 Linux)中,每个文件或目录都有一个相关的 inode (索引节点),其中包含了大部分文件的元数据(metadata),例如:
- 文件类型(普通文件,目录,符号链接,设备文件等)
- 文件权限(读,写,执行权限等)
- 文件的所有者和群组
- 文件的大小
- 文件的时间戳,包括最后访问时间(atime)、最后修改时间(mtime)和状态最后改变时间(ctime)
- 文件的链接数(硬链接)
- 文件的实际数据块的位置
注意,文件的名字并不存储在 inode 中,而是存储在目录的数据部分。
每个 inode 都有一个唯一的 inode 号,可以用来唯一标识一个文件。实际上,当你通过文件名访问一个文件时,操作系统首先会在目录数据中查找文件名对应的 inode 号,然后通过 inode 号找到 inode,再通过 inode 找到文件的实际数据。
可以使用 ‘ls -i’ 命令查看文件的 inode 号,例如:
1 | $ ls -i /etc/passwd |
(2)目录项
在Unix-like系统中,每个目录项通常包括以下两个部分:
- 文件名(filename):表示文件的名称。这是人类可读的文件标识符,用于通过文件系统浏览和访问文件。
- Inode号(inode number):是文件在文件系统中的唯一标识符,存储文件的元数据,如文件大小、所有者、权限等,并指向存储文件数据的物理块。
目录实际上就是一组目录项的列表,每个目录项都链接到一个inode,这个inode可以是文件,也可以是其他目录。这样,通过目录和目录项,文件系统能够实现文件和目录的组织和查找。
值得注意的是,目录项并不包含文件内容或文件属性(如修改日期、权限等)。这些信息都存储在inode中
(3)FAT与UFS
FAT文件系统:
FAT是一种由微软公司创建的老式文件系统,主要用于微软的操作系统中,如DOS和早期的Windows。FAT文件系统有几个不同的版本,包括FAT12(用于早期的PC-DOS和MS-DOS系统),FAT16和FAT32。FAT的主要特点是简单且兼容性强,这使得它在许多嵌入式系统和可移动存储设备(如U盘和SD卡)中得到广泛应用。然而,FAT文件系统也有其局限性,比如在FAT32中,单个文件的最大大小限制为4GB,且不支持权限管理等。
UFS文件系统:
UFS是一种主要用于Unix和Unix-like系统(包括BSD、Solaris等)的文件系统。它是基于更早期的FFS(Fast File System)发展而来的,支持更多的现代文件系统特性,如日志(journalling,能提高文件系统的健壮性和恢复速度),软链接(symbolic links)和硬链接(hard links),以及更复杂的权限管理。UFS使用inode来存储文件元数据,支持大文件(多于4GB)的存储。
7.硬链接与符号链接
(1)硬链接
创建一个文件touch big1.c
可以使用stat big1.c
查看文件属性,其中硬链数也存在
给该文件做一个硬链接ln
1 | $ ln big1.c big1_link |
查看big1_link
的文件属性
可以知道此时big1_link
文件的硬链接数量+1,且其inode号与big1.c相同
硬链接可以使得多个不同的文件关联同一个inode信息,类似多个指针指向同一个空间
将原文件big1.c
删除,查看big1_link
文件属性
硬链接数量-1,但是仍然可以通过big1_link
操作inode
号指向的物理内存
硬链接为目录项的同义词
硬链接:硬链接是一个文件系统中的条目或引用,指向存储数据的inode。原始文件和硬链接共享相同的inode和存储空间,它们在文件系统中是平等的,没有主从之分。删除任何一个都不会影响其他的。但是,硬链接不能跨文件系统,也不能链接到目录(不能给分区也不能给目录建立)
(2)符号链接
符号链接:符号链接和硬链接不同,它是一个单独的文件,包含了另一个文件的路径(可以理解为一个指向另一个文件或目录的指针)。可以跨文件系统,可以链接目录。如果删除了符号链接指向的原始文件,那么符号链接将会失效
在windows下类似于软件的快捷方式
创建符号链接ln -l
1 | $ ln -l big1.c big1_link |
(3)link()与unlink()函数
在C语言中,link和unlink是两个系统调用函数,它们用于操作Unix/Linux文件系统中的硬链接
- link():此函数用于创建一个新的硬链接。函数原型如下
1 |
|
其中,existing参数是现有文件的路径,new参数是新创建的硬链接的路径。成功时,这个函数返回0,如果出现错误,它返回-1,并设置errno为指示错误类型的值
- unlink():此函数用于删除一个文件的硬链接。函数原型如下
1 |
|
其中,pathname参数是需要删除链接的文件路径。如果成功,此函数返回0,如果失败,它返回-1,并设置errno为指示错误的值
注意,当一个文件的硬链接被删除,文件系统会减少文件inode的链接计数。只有当这个链接计数减少到0时(也就是说,没有硬链接指向该文件),文件系统才会真正删除该文件,并释放它所占用的磁盘空间
(4)remove与rename函数
remove()和rename()都是标准库函数,它们在<stdio.h>头文件中声明。它们用于文件操作,如删除文件或重命名文件
- remove(): 此函数用于删除一个文件或者目录。函数原型如下
1 | int remove(const char *pathname); |
其中,pathname是需要删除的文件或目录的路径。如果成功,此函数返回0。如果失败,它返回-1,并设置errno为指示错误的值
- rename(): 此函数用于重命名一个文件或目录。函数原型如下\
1 | int rename(const char *oldpath, const char *newpath); |
其中,oldpath是现有文件或目录的路径,newpath是新的名称。如果成功,此函数返回0。如果失败,它返回-1,并设置errno为指示错误的值
请注意,remove()和rename()函数的行为可能会因文件系统类型和操作系统而有所不同。例如,rename()在某些系统上可能无法跨文件系统移动文件,或者如果newpath已经存在,它的行为可能会因系统而异。因此,当使用这些函数时,您应该总是检查它们的返回值以确定操作是否成功,并适当地处理错误
8.utime
可以更改文件最后的读的时间与最后修改的时间
1 |
|
9.目录的创建和销毁
(1)mkdir与rmdir
在C语言中,mkdir() 和 rmdir() 函数是用来创建和删除目录的。这两个函数在 <sys/stat.h>
和 <sys/types.h>
头文件中声明。
mkdir()
: 这个函数用于创建一个新的目录。函数的原型如下:
1 | int mkdir(const char *pathname, mode_t mode); |
其中,pathname
是要创建的新目录的路径,mode
是新目录的权限位。如果函数成功,则返回 0。如果失败,则返回 -1,并设置 errno
为表示错误的值。
rmdir()
: 这个函数用于删除一个已存在的目录。函数的原型如下:
1 | int rmdir(const char *pathname); |
其中,pathname
是要删除的目录的路径。如果函数成功,则返回 0。如果失败,则返回 -1,并设置 errno
为表示错误的值。
注意:在使用 rmdir()
删除一个目录时,该目录必须为空,也就是说,它不包含任何其他的文件或目录。如果目录非空,则函数将失败。
10.更改当前工作路径
(1)chdir与fchdir
在C语言中,chdir()和fchdir()都是用来改变当前的工作目录的
chdir()
: 这个函数用于改变当前的工作目录。函数的原型如下:
1 | int chdir(const char *path); |
其中,path
是要设为新的工作目录的路径。如果函数成功,则返回 0。如果失败,则返回 -1,并设置 errno
为表示错误的值。
可以更改当前进程的工作目录,若需要将一个进程设置为守护进程,需要一直运行,这个时候可以将这个进程切换到一个一直存在不会被轻易卸载的路径
fchdir()
: 这个函数也是用于改变当前的工作目录,但是它接收一个已经打开的目录文件的文件描述符作为参数,而不是一个路径。函数的原型如下:
1 | int fchdir(int fd); |
其中,fd
是一个已经打开的目录文件的文件描述符。如果函数成功,则返回 0。如果失败,则返回 -1,并设置 errno
为表示错误的值。
chdir()
和 fchdir()
函数都声明在 <unistd.h>
头文件中
(2)getcwd
在C语言中,getcwd()
函数用于获取当前工作目录的绝对路径。这个函数在unistd.h
头文件中声明。
getcwd()
函数的原型如下:
1 | char *getcwd(char *buf, size_t size); |
参数说明:
buf
是一个字符指针,指向一个预先分配的缓冲区,这个缓冲区用于存储获取的路径。如果buf
参数为NULL,新的缓冲区可能会被malloc(),需要在使用完后free()这个缓冲区。size
参数是缓冲区的大小。如果路径的长度超过这个大小,函数将返回NULL,并将errno
设置为ERANGE
。
函数的返回值是一个指向表示当前工作目录的字符串的指针。如果出错,它会返回 NULL,并设置 errno
来表示错误
示例:
1 |
|
这个程序会打印出当前的工作目录。注意我们在使用完 cwd
后调用 free()
函数来释放内存
11.分析目录/读取目录内容
(1)glob
glob()
函数是C语言中用于查找文件系统中与给定模式匹配的路径名的函数。这个函数在文件系统中查找文件名,并返回一个包含所有匹配的路径名的列表
函数原型:
1 | int glob(const char *pattern, int flags, |
参数说明:
pattern
:这是要匹配的模式字符串。它可以包含各种特殊字符,如星号(*),问号(?)和方括号([])等,这些都可以用于匹配一个或多个字符。flags
:这是影响glob()
函数行为的标志。例如,0
表示不需要进行其他扩展,GLOB_TILDE
会扩展波浪号(~),GLOB_BRACE
会扩展花括号表达式,等等。errfunc
:这是一个函数指针,可以用来处理在查找过程中发生的错误。如果这个参数为NULL
,glob()
将忽略所有错误。,若发生错误,会将发生错误的路径传给epath,错误号赋值给errnopglob
:这是一个指向glob_t
结构体的指针,用于接收匹配的结果。glob_t
结构体包含了匹配到的路径名的数量和指向路径名数组的指针。typedef struct { size_t gl_pathc; /* Count of paths matched so far */ char **gl_pathv; /* List of matched pathnames. */ size_t gl_offs; /* Slots to reserve in `gl_pathv'. */ } glob_t;
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
- `gl_pathc`:这个字段表示匹配到的路径的数量。
- `gl_pathv`:这是一个指向字符串数组的指针,这个数组包含了所有匹配的路径名。数组以一个空指针(NULL)结束,因此你可以像遍历常规的以 NULL 结束的 C 字符串数组一样遍历它。
- `gl_offs`:这个字段被 `glob()` 函数用来确定在 `gl_pathv` 中预留多少个空槽位。如果 `GLOB_DOOFFS` 标志被设置,那么 `gl_pathv` 数组的前 `gl_offs` 个元素将被初始化为 NULL。
需要注意的是,`glob()` 函数会为 `gl_pathv` 数组分配内存,因此在使用完这个结构后,需要调用 `globfree()` 函数来释放这部分内存,防止内存泄漏。
`glob()`函数执行成功后,返回0,失败时返回一个错误代码
示例:
```c
#include <stdio.h>
#include <stdlib.h>
#include <glob.h>
#include <string.h>
// 模式是当前目录下 以b开头的文件
#define PAT "./b*"
// 回调函数
#if 1
static int errfunc_(const char *errpath,int errno)
{
puts(errpath);
fprintf(stderr,"ERROR MSG:%s\n",strerror(errno));
return 0;
}
#endif
int main(int argc,char **argv)
{
int err;
glob_t globres;
// err==0表示成功 否则失败
err = glob(PAT,0,errfunc_,&globres);
if(err)
{
printf("ERROR CODE = %d \n",err);
exit(1);
}
// 将glob_t结构体的信息打印
for(int i = 0;i<globres.gl_pathc;i++)
puts(globres.gl_pathv[i]);
globfree(&globres);
exit(0);
}
(2)globfree
1 | void globfree(glob_t *pglob); |
这个函数用于释放 glob()
函数为 pglob
结构分配的内存
二、系统数据文件和信息
1.时间函数精讲
(1)time
time
函数是一个系统调用,可以将系统时间获得,并且存储在一个time_t
存储日历时间的算术类型中
time()
函数会返回系统的当前日历时间,自1970年1月1日以来的秒数(也被称为UNIX时间或POSIX时间)。如果系统没有时间,则返回-1。
1 | NAME |
参数 tloc
是一个指向 time_t
类型的指针,如果该指针不为空,那么返回的时间值也会存储在这个指针指向的位置。如果传入NULL,那么函数只会返回时间值。
使用方法:
1 |
|
示例:
1 |
|
在这个示例中,我们获取了当前的时间,并打印出来。如果 time()
函数返回 -1
,我们就输出一个错误消息
(2)gmtime
在C语言中,gmtime()
函数用于将time_t
类型的时间转换为一个表示UTC时间的tm
结构体
函数原型
1 |
|
函数接受一个指向time_t
类型的指针timer
,并返回一个指向tm
结构体的指针。这个结构体包含了转换后的时间(UTC时间)
tm
结构体包含成员如下:
1 | struct tm { |
示例:
1 |
|
这个例子获取当前的时间,转换为UTC时间,并打印出来
(3)localtime
在C语言中,localtime()
函数被用于将一个 time_t
类型的时间转换为表示本地时间的 tm
结构
1 |
|
localtime()
函数接收一个指向 time_t
类型的指针 timer
,并返回一个指向 tm
结构体的指针。这个结构体包含了转换后的时间(即本地时间)
示例:
1 |
|
这个示例获取了当前的时间,并将其转换为本地时间,然后将其打印出来
(4)mktime
mktime
是C语言中的一个函数,它用于将一个 tm
结构体(通常由 gmtime
或 localtime
返回)转换为一个 time_t
类型的值(该值表示从Epoch(1970年1月1日 00:00:00 UTC)到指定时间的秒数)
1 |
|
这个函数的作用是将 tm
结构体表示的本地时间转换为 time_t
类型表示的自Epoch以来的秒数。如果转换成功,函数会返回转换后的 time_t
值。如果转换失败,函数会返回 (time_t) -1
注意,mktime
会调整 tm
结构体的 tm_wday
和 tm_yday
字段,并可能调整其他字段,以便所有字段的值都处于正常范围内(自动调整肯不正确的结构体元素)
示例
1 |
|
这个例子将日期 “2023-07-27 12:00:00” 转换为自Epoch以来的秒数,并打印出转换后的时间
(5)strftime
strftime
是C语言中的一个函数,它可以根据给定的格式字符串将一个 struct tm
结构体格式化为一个字符串。这个函数对于日期和时间的格式化非常有用
1 |
|
参数的意义如下:
s
是用来存储格式化字符串的缓冲区。maxsize
是缓冲区的大小。format
是格式字符串,它定义了输出的格式。tim_p
是一个指向struct tm
结构体的指针,这个结构体包含了要格式化的时间。
这个函数将根据 format
中的格式说明符将时间格式化为字符串,并将结果存储在 s
中。如果格式化的字符串(包括终止的空字符)的长度大于 maxsize
,那么 strftime
会返回0,并且 s
的内容可能是未定义的
以下是一些常见的格式说明符:
%Y
:四位数的年份%m
:两位数的月份%d
:两位数的日期%H
:24小时制的小时%M
:两位数的分钟%S
:两位数的秒%A
:完整的星期几名称%B
:完整的月份名称
示例
1 |
|
这个例子首先获取当前的时间(自Epoch以来的秒数),然后将其转换为本地时间,最后使用 strftime
将时间格式化为一个字符串并打印出来
(6)编写程序在一个指定文件中输入当前的时间变化
执行程序后可以重新打开一个终端使用以下命令进行跟踪当前文件内容的变化
1 | $ tail -f {文件路径} |
程序代码
1 |
|
make之后,执行tail -f ./out
1 | (base) zxz@zxz-B660M-GAMING-X-AX-DDR4:~/Proj/CLionProj/UNIX/IO/fs$ tail -f ./out |
三、进程环境
1.main函数
int main(int argc,char *argv[])
2.进程的终止
正常终止:
从main
函数return
调用exit()
调用_exit
或者_EXIT
最后一个线程从其启动例程返回
最后一个线程调用pthread_exit
异常终止:
调用abort
接到一个信号并且终止
最后一个线程对其取消请求作出响应
(1)父子进程
在操作系统中,进程(Process)是程序的一次运行实例,可以被操作系统管理和调度的基本单位。每个进程都有其自己的内存空间和系统资源。进程间不能直接访问彼此的资源,只能通过进程间通信(如:管道、消息队列、信号等)来进行数据交换
在Unix/Linux系统中,进程之间的关系通常为父子关系,即一个进程是由另一个进程创建的。
父进程:是创建其他进程(子进程)的进程。
子进程:是由其他进程(父进程)创建的进程。
子进程和父进程的主要区别是它们的内存空间和系统资源。子进程是父进程的复制品,它获得父进程数据的副本(例如变量,环境变量等),但是子进程并不能访问父进程的数据段,代码段。换句话说,父进程和子进程有各自独立的运行环境。
在C语言中,通常使用fork
函数来创建子进程。fork
函数会创建一个新的进程,新进程是调用fork
的进程(父进程)的一个复制品。
示例
1 |
|
在上面的例子中,fork
函数被调用一次,但返回两次。一次是在父进程中,一次是在子进程中。在子进程中,fork
函数返回0,在父进程中,fork
函数返回子进程的PID
(2)钩子函数
钩子函数也被称为回调函数,它是一种将函数指针作为参数传递给其他函数的技术。当特定的事件或条件发生时,这个回调函数会被调用,以改变或扩展原有的执行流程
exit
在C语言中,exit()
函数被用于结束一个程序。这个函数接受一个整型参数,这个参数会被作为程序的退出状态返回给操作系统。函数原型如下:
1 | void exit(int status); |
status
:这是一个整型参数,可以是任意整型值。这个值通常是EXIT_SUCCESS
(这是一个宏,通常被定义为0),EXIT_FAILURE
(这也是一个宏,通常被定义为非0值)或者其他一些特定的错误代码。这个退出状态值可以被其他程序(比如shell)获取,以了解程序是如何退出的。
当调用exit()
函数时,程序会立即终止,并且完成一些清理工作,包括:
- 关闭所有的标准I/O流(如stdin、stdout和stderr等)。
- 调用由
atexit()
函数注册的所有退出处理程序(如果有的话)。
需要注意的是,exit()
函数在终止程序后不会返回,所以它位于程序代码的任何位置,都会立即结束程序
atexit
atexit()
函数是C语言库中的一个函数,用于注册一些在程序正常结束时(例如,当return
从main()
返回或者调用exit()
时)需要被调用的函数。这些注册的函数被称为终止处理程序或退出处理程序。在程序终止时,这些函数将按照与注册(声明)相反的顺序被调用。这个函数的作用被称之为钩子函数,一个进程正常终止前会调用钩子函数,释放该释放的内容
钩子函数顾名思义,去下来的顺序与挂上去的顺序相反,释放顺序类似于栈,主要用于释放资源
函数原型
1 | int atexit(void (*func)(void)); |
它在C标准库<stdlib.h>中定义。参数是一个无参且无返回值的函数指针(即,一个接受void
参数并返回void
的函数)。如果注册成功,atexit()
返回0,否则返回非零值
示例程序:
1 |
|
make之后执行
1 | (base) zxz@zxz-B660M-GAMING-X-AX-DDR4:~/Proj/CLionProj/UNIX/IO/fs$ make gouzi |
分析:
程序中写到
1 | atexit(f1()); |
并不是调用,着相当于把这些函数挂到钩子上,当程序正常结束的时候,才会按照挂上去的相反顺序取下(调用)
(3)abort
abort
函数是C标准库中的一个函数,用于使程序异常终止,并生成一个核心转储文件。它不调用任何由atexit
或者on_exit
函数注册的函数,也不刷新任何打开的流。函数原型如下:
1 | void abort(void); |
abort
函数没有参数。它会发送一个SIGABRT
信号给调用进程,除非这个信号被捕获并且信号处理程序没有返回,否则会使程序异常终止。
请注意,abort
函数的调用不会返回。
一个简单的例子如下:
1 | #include <stdio.h> |
在上面的例子中,程序会在调用abort()
之后立即结束,并可能生成一个核心转储文件,所以最后一行printf的语句不会被执行
3.命令行参数的分析
(1)getopt
(2)getopt_long
4.环境变量
(1)查看环境变量
在Ubuntu(以及其他大多数Linux系统)中,环境变量是存储系统行为信息的全局值。这些环境变量可以被系统、应用程序、Shell脚本等使用。环境变量存在于每个运行的进程环境中,子进程会从父进程那里继承这些变量
以下是一些常见的环境变量:
PATH
:这是一个由冒号分隔的目录列表,当你要运行一个可执行文件时,Shell会在PATH
中列出的这些目录中查找。HOME
:当前用户的主目录的路径。PWD
:当前工作目录的路径。USER
:当前用户的用户名。SHELL
:当前用户默认的shell。LANG
和LC_*
:这些变量用于定义用户的语言和地区设置,包括货币、日期格式、字符编码等。LD_LIBRARY_PATH
:这个变量包含了动态链接库的搜索路径
可以使用printenv
命令来查看当前的环境变量:
1 | $ printenv |
你也可以使用echo
命令查看环境变量:
1 | $ echo $PATH |
也可以在终端输入export
查看当前系统的环境变量
1 | $ export |
你可以使用export
命令设置新的环境变量,或修改已有的环境变量。例如:
1 | export VARNAME=value |
请注意,通过这种方式设置的环境变量只会对当前Shell及其子进程有效。如果你希望在整个用户会话或系统范围内设置环境变量,你需要在特定的配置文件中(如~/.bashrc
,~/.profile
,/etc/environment
等)设置这些变量。具体取决于你的使用场景和需求。
(2)编写程序输出环境变量
1 |
|
在C语言中,environ
是一个全局的外部变量,包含了所有的环境变量。它是一个字符指针数组,每个指针都指向一个形如“name=value”的字符串,这个字符串就代表一个环境变量的名称和对应的值。这个数组以NULL指针作为结束标志。
下面是一个简单的例子,展示了如何使用environ
来访问环境变量:
1 | #include <stdio.h> |
这个程序会打印出所有的环境变量及其对应的值。请注意,在编写依赖于environ
的程序时,你需要注意环境变量可能会被其他程序改变,所以依赖于特定环境变量的程序需要在使用这些环境变量之前进行检查。
另外,如果你只是需要获取特定的环境变量,C标准库提供了getenv
函数,可以直接获取指定的环境变量:
1 | #include <stdio.h> |
这个例子会打印出PATH环境变量的值
5.C程序的存储空间布局
(1)虚拟地址空间
在32位系统中,虚拟地址空间的大小是由地址的位数决定的。一般来说,N位的地址可以表示2^N
个地址。因此,32位系统可以表示2^32
个地址
2^32
是4294967296
,换算成字节,就是4GB的地址空间。这是理论上的最大值,实际上可用的地址空间可能会因为操作系统的设计和配置而有所不同
例如,在Windows和Linux操作系统中,32位系统的用户空间通常是2GB或者3GB,剩下的1GB或2GB被保留给内核使用。具体的分配方式取决于操作系统的设计以及配置参数
需要注意的是,虽然32位系统的虚拟地址空间最大是4GB,但是这并不意味着系统就能支持4GB的物理内存。物理内存的支持量取决于多种因素,包括CPU的寻址能力、主板的设计以及操作系统的配置等
(2)虚拟地址空间布局
在一个典型的32位Linux操作系统中,虚拟地址空间分布如下:
- 用户空间:这是虚拟地址空间的低地址部分,通常包括从0x00000000到0xBFFFFFFF的地址。这部分地址空间主要用于存放用户程序的代码、数据、堆和栈等。用户空间的大小通常是3GB。
- 内核空间:这是虚拟地址空间的高地址部分,通常包括从0xC0000000到0xFFFFFFFF的地址。这部分地址空间主要用于存放内核代码和数据,以及用于操作系统管理的各种数据结构。内核空间的大小通常是1GB。
在用户空间中,地址布局通常如下:
- 低地址部分(通常从0x00000000开始)是代码段,这里存放的是程序的机器代码。
- 代码段之后是数据段,这里存放的是程序的全局变量等初始化的数据。
- 数据段之后是BSS段,这里存放的是程序未初始化的全局变量。
- BSS段之后是堆(heap)区域,这是动态分配(如malloc和new等操作)的内存区域。堆从低地址向高地址增长。
- 高地址部分(通常接近0xBFFFFFFF)是栈(stack)区域,这是存放函数调用栈和局部变量的内存区域。栈从高地址向低地址增长。
- 堆和栈之间的部分,通常称为未映射区域,这部分地址空间是空闲的,可以被映射到文件或者被用于扩展堆等。
以上都是虚拟地址空间的布局,实际的物理内存是由操作系统的内存管理子系统负责分配和回收的。虚拟地址到物理地址的映射是由硬件的内存管理单元(MMU)完成的。
6.库
动态库
静态库
(1)手工装载库
在C语言中,通常我们会静态地或动态地链接到一个库。静态链接会在编译时将库的代码整合到可执行文件中,而动态链接则会在程序运行时将库代码链接到程序中。但除此之外,还有一种手工装载库的方式,它允许我们在程序运行时动态地加载或卸载库。这种方式在Linux系统中主要依赖于dlopen()
, dlsym()
, dlclose()
和 dlerror()
这几个函数,它们定义在dlfcn.h
头文件中
以下是如何使用这些函数进行手工装载库的简单示例:
1 |
|
在这个示例中,我们使用了dlopen()
函数打开一个库(这里是数学库libm.so
),然后使用dlsym()
获取cos
函数的地址,并通过一个函数指针调用它。完成后,我们使用dlclose()
关闭库
注意:当你编译这个程序时,你可能需要链接dl
库,如下所示:
1 | gcc -o myprogram myprogram.c -ldl |
手工装载库在很多应用中都很有用,例如插件系统,它允许软件在运行时加载额外的功能或模块
7.函数跳转
(1)setjmp与longjmp
setjmp
和 longjmp
是C语言中用于非局部跳转的函数,它们定义在 setjmp.h
头文件中。这两个函数通常配对使用,可以用于从深度嵌套的函数调用中跳回到一个预定的位置,这种跳转方式通常被用于处理错误和异常
setjmp
1 |
|
setjmp
是 C 语言中用于保存当前的程序执行环境的函数,这个环境可以被 longjmp
函数用来进行非局部跳转。setjmp
定义在 setjmp.h
头文件中
这里,jmp_buf
是一个宏,用于保存 setjmp 环境相关的信息,包括堆栈、寄存器等信息
setjmp
函数会保存当前的程序执行环境到 jmp_buf
类型的变量中,然后返回0。
一旦执行环境被保存,就可以使用 longjmp
函数进行跳转,这会使得程序跳转回 setjmp
的位置,并且 setjmp
的返回值会被设置为 longjmp
的第二个参数的值。
longjmp
1 |
|
longjmp
是C语言中用于实现非局部跳转的函数。它可以使程序从深度嵌套的函数调用中跳出,回到一个预定的、固定的位置,这个位置就是之前用 setjmp
函数设定的位置。
longjmp
函数的第一个参数是 setjmp
保存环境信息的 jmp_buf
类型变量,第二个参数 val
是设定的返回值,它将会成为 setjmp
的返回值。需要注意的是,val 的值不能为0,如果val 为0,那么 setjmp
返回值为1。
setjmp
执行一次返回两次,第一次是调用的时候直接返回0,第二次是longjmp
跳转到setjmp
标记的位置时,将longjmp
的第二个参数val
作为返回值返回
使用 longjmp
会使得程序回到最近一次 setjmp
的位置,并且 setjmp
会返回 longjmp
的第二个参数 val
的值。
(2)代码测试
查看程序的压栈与弹栈效果:
1 |
|
该程序的实现逻辑是:在程序中a调用b,b调用c,c调用d,d最后返回。从程序执行效果可以看出,前部分为相互嵌套调用的顺序,后半部分为出栈的顺序
1 | (base) zxz@zxz-B660M-GAMING-X-AX-DDR4:~/Proj/CLionProj/UNIX/IO/fs$ ./myjmp |
在上述代码的基础上进行修改程序
在函数d()
中进行longjmp
,在函数a()
中设置跳转点即在函数a()
中进行setjmp
的实现
1 |
|
make之后执行程序运行结果
1 | (base) zxz@zxz-B660M-GAMING-X-AX-DDR4:~/Proj/CLionProj/UNIX/IO/fs$ ./jmp |
可以对比压栈与弹栈效果
的程序,原来的弹栈顺序为d->c->b->a
,现在程序直接从d
跳转到了a