一、目录和文件
1.1 文件属性(stat)
stat() 可以通过文件名获取文件的属性。
fstat() 可以通过打开的文件描述符获取文件的属性。
lstat() 和 stat() 功能相同,有一点区别就是当 pathname 是一个符号链接文件的时候,lstat() 返回的是符号链接文件本身的属性,而不是链接文件指向的文件的属性。而 stat() 则是返回符号链接所指向文件的属性。
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
int stat(const char *pathname, struct stat *statbuf);
int fstat(int fd, struct stat *statbuf);
int lstat(const char *pathname, struct stat *statbuf);
下面是 stat 结构体中的部分成员:
struct stat {
dev_t st_dev; /* ID of device containing file */
ino_t st_ino; /* Inode number */
mode_t st_mode; /* File type and mode */
nlink_t st_nlink; /* Number of hard links */
uid_t st_uid; /* User ID of owner */
gid_t st_gid; /* Group ID of owner */
dev_t st_rdev; /* Device ID (if special file) */
off_t st_size; /* Total size, in bytes */
blksize_t st_blksize; /* Block size for filesystem I/O */
blkcnt_t st_blocks; /* Number of 512B blocks allocated */
struct timespec st_atim; /* Time of last access */
struct timespec st_mtim; /* Time of last modification */
struct timespec st_ctim; /* Time of last status change */
}
我们可以通过 stat 结构体中的 st_size 成员获取文件长度,程序 flen.c 如下:
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
off_t flen(char *path){
struct stat fstat;
if (stat(path, &fstat) < 0) {
perror("stat()");
exit(1);
}
return fstat.st_size;
}
int main(int argc, char * argv[])
{
if (argc < 2) {
perror("flen <filepath>");
exit(1);
}
off_t fsize = flen(argv[1]);
printf("file size: %lld\n", (long long)fsize);
exit(0);
}
但 st_size 仅仅是文件的属性,实际占用磁盘的块大小和个数是 st_blksize 和 st_blocks。如下图,文件 flen.c 的 Size 为 429 B,但是占用磁盘块数为 8个,8 * 512B = 4096B:
下面是一段创建空洞文件的代码 hole_file.c,该程序会创建一个大小为 5g 的空洞文件(注意 5LL * 1024LL * 1024LL * 1024LL 一定要加单位 LL,否则计算结果会溢出):
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main(int argc, char *argv[])
{
if (argc < 2) {
perror("hole_file <filepath>");
exit(1);
}
int fd = open(argv[1], O_RDWR | O_CREAT);
if(fd < 0){
perror("open");
exit(1);
}
/* make a 5g hole file */
lseek(fd, 5LL * 1024LL * 1024LL * 1024LL, SEEK_SET);
write(fd, "", 1);
exit(0);
}
创建空洞文件 tmp 后使用 stat 查看其信息,可以看到 size 为 5g,但是实际在磁盘上仅占用了 8 个块,一共 4kb:
1.2 文件访问权限(umask)
使用 ls -l 查看文件的权限信息可以得到如下结果,第一位用于指出文件的类型,后 9 位分别指出 文件所有者的权限、同组用户的权限以及其他人的权限。这些内容存储在 stat 结构体的 st_mode 成员中,st_mode 是一个 16 位的位图,用于表示文件类型,文件访问权限,及特殊权限位。
文件类型分为如下几种:
- d:目录文件;
- c:字符设备文件;
- b:块设备文件;
- -:常规文件;
- l:符号链接文件;
- s:网络套接字(socket)文件;
- p:pipe 管道文件;
umask() 函数可以设置文件创建时的权限,防止产生权限过松的文件。并且返回之前的 mask 值。
#include <sys/types.h>
#include <sys/stat.h>
mode_t umask(mode_t mask);
1.4 文件权限的管理(chmod)
文件的权限位中,1 对应 x(执行),2 对应 w(可写),4 对应 r(可读),我们可以通过 chmod 指令来修改文件的权限位信息,如下:
Linux 操作系统也提供了 chmod() 和 fchmod() 函数来使得我们能在程序中修改文件的权限。
chmod() 可以通过文件路径修改文件的权限。
fchmod() 可以通过打开的文件描述符来修改文件权限。
#include <sys/stat.h>
int chmod(const char *pathname, mode_t mode);
int fchmod(int fd, mode_t mode);
1.5 文件系统(FAT、UFS)
文件系统:文件或数据的存储和管理。
FAT16/32:静态单链表,闭源,惧怕大文件。
UFS:分区,位图,inode,块。
面试题:统计无符号整数二进制中 1 的个数。
去一法:
#include <stdio.h>
int main()
{
unsigned int n;
scanf("%u\n", &n);
int count = 0;
while (n != 0) {
n = n & (n-1);
count++;
}
printf("一的个数为:%d\n", count);
return 0;
}
运行结果:
1.6 硬链接,符号链接(unlink、remove)
使用 ln 命令创建文件 file 的硬链接文件:
然后使用 stat 命令查看两个文件的信息:
可以发现两个文件的 inode 号是相同的,并且硬链接数 links 数变成了 2。由此可以知,硬链接在目录下创建一个目录项,该目录项指向的 inode 与被链接的文件的 inode 相同,所以两个文件 file 和 file_link 对应的是同一个磁盘上的文件,两者本质上是同一个文件。
硬链接是目录项的同义词,并且建立硬链接有限制,不能跨分区建立,不能给目录建立。
然后再使用 ln -s 来创建 file 的符号链接:
使用 stat 查看符号链接文件的信息,可以发现其指向的 inode 和原文件 file 不同,并且 links 为 1:
符号链接可跨分区,可给目录建立。
其实符号链接类似于 windows 中的快捷方式,相当于重新创建了一个新文件,文件内容为被链接文件的路径,使用 readlink -f 可以查看符号链接本身的内容:
unlink 系统调用可以删除文件名所指向文件的硬链接,如果删除后文件硬链接数为 0,则文件被删除(从磁盘上删除),如果删除后硬链接数不为 0,则文件不被删除。
#include <unistd.h>
int unlink(const char *pathname);
另外,unlink 可以帮助我们创建一个临时文件,在程序中先打开一个新文件,然后立刻用 unlink 删除文件的硬链接,但此刻文件不会立刻被删除(因为进程打开了这个文件,链接数不为 0),进程结束后该文件才被释放。
真正实现删除文件的函数是 remove(rm 命令就是用这个封装的):
#include <stdio.h>
int remove(const char *pathname);
1.7 目录的创建和销毁(mkdir、rmdir)
mkdir 系统调用可以创建一个目录。
#include <sys/stat.h>
#include <sys/types.h>
int mkdir(const char *pathname, mode_t mode);
rmdir 系统调用可以删除一个空目录。
#include <unistd.h>
int rmdir(const char *pathname);
1.8 更改当前工作路径(chdir)
chdir 系统调用可以更改当前进程的工作目录(cd 命令)。
#include <unistd.h>
int chdir(const char *path);
int fchdir(int fd);
getcwd 系统调用可以获取当前工作目录(pwd 命令)。
#include <unistd.h>
char *getcwd(char *buf, size_t size);
1.9 分析目录/读取目录内容(glob、opendir、readdir、rewinddir、seekdir、telldir)
glob(3) 函数可以找到所有与 pattern 所匹配的路径名(pattern 中一般包含通配符,如 * ? 等)。
#include <glob.h>
int glob(const char *pattern, int flags,
int (*errfunc) (const char *epath, int eerrno),
glob_t *pglob);
void globfree(glob_t *pglob);
结构体 glob_t 存放返回结果,如下,gl_pathc 和 gl_pathv 类似于 argc 和 argv,gl_pathc 存放路径名的个数,gl_pathv 存放路径名的地址。
typedef struct {
size_t gl_pathc; /* Count of paths matched so far */
char **gl_pathv; /* List of matched pathnames. */
size_t gl_offs; /* Slots to reserve in gl_pathv. */
} glob_t;
例子(glob.c):使用 glob 函数找出当前目录下的所有 .c 文件,注意参数不能从 shell 中传,只能在程序内部定义,因为 shell 在传值之前会先进行通配符转换(如将 *.c 变成实际的文件路径)。
#include <stdio.h>
#include <stdlib.h>
#include <glob.h>
static int errfunc(const char *epath, int eerrno)
{
printf("%s\n", epath);
return 0;
}
int main()
{
glob_t pglob;
if (glob("./*.c", 0, NULL, &pglob) != 0) {
perror("glob");
exit(1);
}
for (int i = 0; i < pglob.gl_pathc; i++) {
printf("%s\n", pglob.gl_pathv[i]);
}
globfree(&pglob);
exit(0);
}
运行结果如下:
opendir(3)、closedir(3) 可以打开和关闭目录流。
#include <sys/types.h>
#include <dirent.h>
DIR *opendir(const char *name);
DIR *fdopendir(int fd);
int closedir(DIR *dirp);
在通过 opendir(3) 打开目录后,可以使用 readdir(3) 函数通过 DIR 指针来读目录项。
#include <dirent.h>
struct dirent *readdir(DIR *dirp);
返回的 struct dirent 成员如下,并且存储在静态区中。
struct dirent {
ino_t d_ino; /* Inode number */
off_t d_off; /* Not an offset; see below */
unsigned short d_reclen; /* Length of this record */
unsigned char d_type; /* Type of file; not supported
by all filesystem types */
char d_name[256]; /* Null-terminated filename */
};
例子(readdir.c):打印对应目录下的所有文件的名字。
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <dirent.h>
int main(int argc, char *argv[])
{
if (argc < 2) {
perror("Usage: readdir <pathname>");
exit(1);
}
DIR *dp;
struct dirent *dirent_p;
if ((dp = opendir(argv[1])) == NULL) {
perror("opendir");
exit(1);
}
while ((dirent_p = readdir(dp)) != NULL) {
printf("%s\n", dirent_p->d_name);
}
exit(0);
}
rewinddir(3) 函数可以重置目录流 dirp 至目录的开头处。这样的话再使用 readdir(3) 函数就会读取第一个目录项。
#include <sys/types.h>
#include <dirent.h>
void rewinddir(DIR *dirp);
seekdir(3) 函数可以设置下一次 readdir(3) 开始读取的位置,其中的 loc 参数必须是 telldir(3) 函数返回的值。
#include <dirent.h>
void seekdir(DIR *dirp, long loc);
telldir(3) 函数返回当前目录流的位置。
#include <dirent.h>
long telldir(DIR *dirp);
二、系统数据文件和信息
2.1 /etc/passwd
下面是 Ubuntu 下的 /etc/passwd 文件的内容,其格式如下:
用户名:加密后的口令:用户id:用户所在组id:注释字段:home目录位置:登录shell
getpwuid(3) 可以通过用户的 uid 获取用户的信息。
getpwnam(3) 可以通过用户的名称 name 获取用户信息。
#include <sys/types.h>
#include <pwd.h>
struct passwd *getpwnam(const char *name);
struct passwd *getpwuid(uid_t uid);
struct passwd 结构体的定义如下(在 <pwd.h> 中,可以看到成员和 /etc/passwd 里面的内容差不多):
struct passwd {
char *pw_name; /* username */
char *pw_passwd; /* user password */
uid_t pw_uid; /* user ID */
gid_t pw_gid; /* group ID */
char *pw_gecos; /* user information */
char *pw_dir; /* home directory */
char *pw_shell; /* shell program */
};
/etc/group 文件可以获取组的信息,getgrgid(3) 和 getgrnam(3) 两个函数也可以获取组信息。
/etc/shadow 文件可以查看用户经过 hash 后的密码。
获取时间戳,time(2) 系统调用,gtime(3),localtime(3) 等。
三、进程环境
3.1 进程的终止( 重要! )
正常终止:
- 从 main 函数返回;
- exit(库函数);
- _exit 或 _Exit(系统调用);
- 最后一个线程从其启动例程返回;
- 最后一个线程调用 pthread_exit;
异常终止:
- 调用 abort(得到一个 coredump 文件);
- 接到一个信号并终止;
- 最后一个线程对取消请求做出响应;
main 函数的返回值是给其父进程看的,如在 shell 中执行程序,其父进程为 shell,使用 echo $? 即可查看 shell 上一个程序执行的返回状态。
exit(3) 函数可以让进程正常终止。
#include <stdlib.h>
void exit(int status);
atexit(3):钩子函数,进程正常终止时,被 atexit(3) 注册的函数会逆序调用(与注册顺序相反),类似于 C++ 的析构函数。
#include <stdlib.h>
int atexit(void (*function)(void));
如下面的程序:
先打印 begin 和 end,直到程序 exit(0) 正常终止后,再依次调用 f3、f2、f1 函数:
要注意的是,在直接调用 _exit(2) 系统调用的时候,是不会执行钩子函数和 IO 清理的。而 exit(3) 则会依次执行,如下图。
3.2 命令行参数的分析
getopt(3)、getopt_long(3) 分别可以获取 “-” 和 “--” 后的命令行参数。
3.3 C 程序的存储空间布局
C 程序的存储空间布局分为以下五个部分:
- 代码区;
- 常量区;
- 静态区,分为已初始化部分(.data)和未初始化部分(.bss);
- 堆区,由程序员动态分配和释放(malloc 和 free);
- 栈区,由编译器自动分配和释放;
pmap 命令可以展示进程的内存分布图。
3.4 库
- 动态库(.so 后缀),在目标代码运行时或加载时链接,生成的可执行文件小,只能在包含相应动态库的环境下运行,并且多个程序可以使用环境中的同一个动态库;
- 静态库(.a 后缀),链接时会拷贝到目标代码中,生成可执行文件大,在不包含相应库的环境下也能运行,多个程序会有多个静态库的拷贝;
- 手工装载库(使用 dlopen(3) 和 dlclose(3) 等函数在程序中动态装载和卸载动态库(shared object));
3.5 函数跳转
setjmp()、longjmp()。