Linux系统编程学习笔记--第四章

4.文件系统

该节对应APUE的第四章------文件和目录,第六章------系统数据文件和信息

4.1 Linux文件目录构成

4.1.1 概述

树状目录结构:

目录解释:

/bin:bin 是 Binaries (二进制文件) 的缩写,这个目录存放着最经常使用的命令。

/boot:这里存放的是启动 Linux 时使用的一些核心文件,包括一些连接文件以及镜像文件。

/dev :dev 是 Device(设备) 的缩写,该目录下存放的是 Linux 的外部设备,在 Linux 中访问设备的方式和访问文件的方式是相同的(一切皆文件)。

/etc:etc 是 Etcetera(等等) 的缩写,这个目录用来存放所有的系统管理所需要的配置文件和子目录。

/home:用户的主目录(家目录),在 Linux 中,每个用户都有一个自己的目录,一般该目录名是以用户的账号命名的,如上图中的 alice、bob 和 eve。

/lib:lib 是 Library(库) 的缩写这个目录里存放着系统最基本的动态连接共享库,其作用类似于 Windows 里的 DLL 文件。几乎所有的应用程序都需要用到这些共享库。

/lost+found:这个目录一般情况下是空的,当系统非法关机后,这里就存放了一些文件。

/media:linux 系统会自动识别一些设备,例如U盘、光驱等等,当识别后,Linux 会把识别的设备挂载到这个目录下。

/mnt:系统提供该目录是为了让用户临时挂载别的文件系统的,我们可以将光驱挂载在 /mnt/ 上,然后进入该目录就可以查看光驱里的内容了。

/opt:opt 是 optional(可选) 的缩写,这是给主机额外安装软件所摆放的目录。比如你安装一个ORACLE数据库则就可以放到这个目录下。默认是空的。

/proc:proc 是 Processes(进程) 的缩写,/proc 是一种伪文件系统(也即虚拟文件系统),存储的是当前内核运行状态的一系列特殊文件,这个目录是一个虚拟的目录,它是系统内存的映射,我们可以通过直接访问这个目录来获取系统信息。

/root:该目录为系统管理员,也称作超级权限者的用户主目录。

/sbin:s 就是 Super User 的意思,是 Superuser Binaries (超级用户的二进制文件) 的缩写,这里存放的是系统管理员使用的系统管理程序。

/tmp:tmp 是 temporary(临时) 的缩写这个目录是用来存放一些临时文件的。

/usr:usr 是 unix shared resources(共享资源) 的缩写,这是一个非常重要的目录,用户的很多应用程序和文件都放在这个目录下,类似于 windows 下的 C:/Windows/ 目录。

/usr/lib理解为C:/Windows/System32

/usr/local:用户级的程序目录,可以理解为C:/Progrem Files/。用户自己编译的软件默认会安装到这个目录下。

/opt:用户级的程序目录,可以理解为D:/Software,opt有可选的意思,这里可以用于放置第三方大型软件(或游戏),当你不需要时,直接rm -rf掉即可。

/var:var 是 variable(变量) 的缩写,这个目录中存放着在不断扩充着的东西,我们习惯将那些经常被修改的目录放在这个目录下。包括各种日志文件。

/run:是一个临时文件系统,存储系统启动以来的信息。当系统重启时,这个目录下的文件应该被删掉或清除。如果你的系统上有 /var/run 目录,应该让它指向 run。

切换用户

cs 复制代码
su 用户名

~代表当前登录用户的用户目录(家目录)。

如果当前的用户是root,则~代表/root:

cs 复制代码
[lighthouse@HongyiZeng ~]$ su root
Password: 
[root@HongyiZeng lighthouse]# cd ~
[root@HongyiZeng ~]# pwd
/root

如果当前的用户是其他用户,则~代表/home/用户名:

cs 复制代码
[lighthouse@HongyiZeng ~]$ pwd
/home/lighthouse

4.1.2 /etc/passwd

/etc/passwd为用户信息文件存放路径。

cs 复制代码
root:x:0:0:root:/root:/bin/bash
bin:x:1:1:bin:/bin:/sbin/nologin
daemon:x:2:2:daemon:/sbin:/sbin/nologin
adm:x:3:4:adm:/var/adm:/sbin/nologin
lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin
sync:x:5:0:sync:/sbin:/bin/sync
shutdown:x:6:0:shutdown:/sbin:/sbin/shutdown
halt:x:7:0:halt:/sbin:/sbin/halt
mail:x:8:12:mail:/var/spool/mail:/sbin/nologin
operator:x:11:0:operator:/root:/sbin/nologin
games:x:12:100:games:/usr/games:/sbin/nologin
ftp:x:14:50:FTP User:/var/ftp:/sbin/nologin
nobody:x:99:99:Nobody:/:/sbin/nologin
systemd-network:x:192:192:systemd Network Management:/:/sbin/nologin
dbus:x:81:81:System message bus:/:/sbin/nologin
polkitd:x:999:998:User for polkitd:/:/sbin/nologin
libstoragemgmt:x:998:997:daemon account for libstoragemgmt:/var/run/lsm:/sbin/nologin
rpc:x:32:32:Rpcbind Daemon:/var/lib/rpcbind:/sbin/nologin
ntp:x:38:38::/etc/ntp:/sbin/nologin
abrt:x:173:173::/etc/abrt:/sbin/nologin
sshd:x:74:74:Privilege-separated SSH:/var/empty/sshd:/sbin/nologin
postfix:x:89:89::/var/spool/postfix:/sbin/nologin
chrony:x:997:995::/var/lib/chrony:/sbin/nologin
tcpdump:x:72:72::/:/sbin/nologin
syslog:x:996:994::/home/syslog:/bin/false
lighthouse:x:1000:1000::/home/lighthouse:/bin/bash

每一行分为7个字段,以冒号:进行分割,含义如下:

|-------|-----------------------------------------------------|
| 字段 | 含义 |
| 用户名 | 用户登录系统时使用的用户名 |
| 密码 | 密码位,通常将passwd文件中的口令字段使用一个x来代替,将/etc/shadow作为真正的口令文件 |
| UID | 用户标识号 |
| GID | 缺省组标识号 |
| 注释性描述 | 例如存放用户全名等信息 |
| 宿主目录 | 用户登录系统后的缺省目录 |
| 命令解释器 | 用户使用的shell,默认为bash |

4.1.3 /etc/group

/ect/group 文件是用户组配置文件,即用户组的所有信息都存放在此文件中。只列出部分:

cs 复制代码
root:x:0:
lighthouse:x:1000:lighthouse

每一行分为4个字段,以冒号:进行分割,含义如下:

|-------|------------------------------------|
| 字段 | 含义 |
| 组名 | 用户组名称 |
| 组密码 | 和 /etc/passwd 文件一样,这里的 "x" 仅仅是密码标识 |
| GID | 组标识号 |
| 组中的用户 | 此字段列出每个群组包含的所有用户 |

4.2 文件和目录

4.2.1 stat

stat显示文件的状态信息。stat命令的输出信息比ls命令的输出信息要更详细。

cs 复制代码
[zoukangcheng@zoukangcheng sysio]# ls
ab  ab.c  dup  dup.c  mycpy  mycpy.c  test
[root@HongyiZeng sysio]# stat ab.c
  文件: 'ab.c'
  大小: 193             块: 8          IO 块: 4096   普通文件
设备: fd01h/64769d    Inode: 797269      硬链接: 1
权限: (0644/-rw-r--r--)  Uid: (    0/    root)   Gid: (    0/    root)
最近访问: 2022-12-02 16:05:47.044135336 +0800
最近修改: 2022-12-02 16:05:45.239158807 +0800
最近改动: 2022-12-02 16:05:45.245158729 +0800
 创建时间: -

系统调用stat用于获取文件的属性。

cs 复制代码
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>

int stat(const char *path, struct stat *buf);
int fstat(int fd, struct stat *buf);
int lstat(const char *path, struct stat *buf);

stat:第一个形参:文件路径; 第二个形参:一个指向结构体stat的指针,因此需要传入结构体的地址;

fstat:第一个形参是文件描述符;

lstat:lstat函数的形参跟stat函数的形参一样。其功能也跟stat函数功能一样,仅有一点不同:stat函数是穿透(追踪)函数,即对软链接文件进行操作时,操作的是链接到的那一个文件,不是软链接文件本身;而lstat函数是不穿透(不追踪)函数,对软链接文件进行操作时,操作的是软链接文件本身。注:软链接严格来说应该叫符号链接。

返回值:成功返回0,失败返回-1,并且将详细错误信息赋值给errno全局变量。

struct stat类型的说明:

cs 复制代码
struct stat {
    dev_t st_dev;     /* 文件的设备编号 */
    ino_t st_ino;     /* 索引结点编号 */
    mode_t st_mode;    /* 文件类型和权限*/
    nlink_t st_nlink;   /*硬链接数 */
    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;   /* 更改时间 */
}; 

代码示例

打印出文件的大小。

cs 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>

// 注意返回值是off_t类型
static off_t flen(const char *fname) {
    struct stat statres; // 声明一个stat类型的结构体statres
    if(stat(fname, &statres) < 0) {
        perror("stat()");
        exit(1);
    }
	// 返回st_size成员
    return statres.st_size;
}

int main(int argc, char **argv) {
    if(argc < 2) {
        fprintf(stderr, "Usage...\n");
        exit(1);
    }

    printf("total size: %lld\n", flen(argv[1]));

    exit(0);
}

off_t类型用于指示文件的偏移量,通常就是long类型,其默认为一个32位的整数,在gcc编译中会被编译为long int类型,在64位的Linux系统中则会被编译为long long int,这是一个64位的整数,其定义在unistd.h头文件中可以查看。

cs 复制代码
# ifndef __off_t_defined
#  ifndef __USE_FILE_OFFSET64
typedef __off_t off_t;
#  else
typedef __off64_t off_t;
#  endif
#  define __off_t_defined
# endif
# if defined __USE_LARGEFILE64 && !defined __off64_t_defined
typedef __off64_t off64_t;
#  define __off64_t_defined
# endif

4.2.2 空洞文件

在描述文件属性的结构体stat中,有以下三个描述文件大小的成员:

cs 复制代码
struct stat {
    off_t st_size;    /* 文件大小,以字节为单位*/
    blksize_t st_blksize; /*文件系统的I/O块大小*/
    blkcnt_t st_blocks;  /* 块数 */
}; 

其中,块大小一般为4096字节,即4KB(一个块为连续8个扇区,每个扇区为512B);块数为该文件的占用的块数;

注意:st_size ≠ st_blksize * st_blocks;或者说,st_size是文件的逻辑大小,而st_blksize * st_blocks是文件的物理大小;

代码示例

创建一个5GB大小的文件:

cs 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main(int argc, char **argv) {
    int fd;

    if(argc < 2) {
        fprintf(stderr, "Usage...");
        exit(1);
    }

    if((fd = open(argv[1], O_WRONLY|O_CREAT|O_TRUNC, 0600)) < 0) {
        perror("open");
        exit(1);
    }
	// 先让指针从文件开头向后移动5G个字节
    lseek(fd, 5LL * 1024LL * 1024LL * 1024LL - 1LL, SEEK_SET);
	// 在最后写入一个空字符
    write(fd, "", 1);

    close(fd);

    exit(0);
}

执行结果:

cs 复制代码
[zoukangcheng@zoukangcheng fs]# make big
cc     big.c   -o big
[zoukangcheng@zoukangcheng fs]# ./big /tmp/bigfile
[zoukangcheng@zoukangcheng fs]# stat /tmp/bigfile 
  File: '/tmp/bigfile'
  Size: 5368709120      Blocks: 8          IO Block: 4096   regular file
Device: fd01h/64769d    Inode: 26199       Links: 1
Access: (0600/-rw-------)  Uid: (    0/    root)   Gid: (    0/    root)
Access: 2022-12-05 16:19:40.734216438 +0800
Modify: 2022-12-05 16:19:40.734216438 +0800
Change: 2022-12-05 16:19:40.734216438 +0800
 Birth: -

可以看出,大小为5368709120B,但是占用的块数却为8,即实际占用的物理大小为4KB * 8 = 32KB。

如果将该文件进行拷贝,再查看拷贝文件的属性:

cs 复制代码
[zoukangcheng@zoukangcheng fs]# cp /tmp/bigfile /tmp/bigfile.bak
[zoukangcheng@zoukangcheng fs]# stat /tmp/bigfile.bak 
  File: '/tmp/bigfile.bak'
  Size: 5368709120      Blocks: 0           IO Block: 4096   regular file
Device: fd01h/64769d    Inode: 26200       Links: 1
Access: (0600/-rw-------)  Uid: (    0/    root)   Gid: (    0/    root)
Access: 2022-12-05 16:26:39.166784814 +0800
Modify: 2022-12-05 16:26:39.166784814 +0800
Change: 2022-12-05 16:26:39.166784814 +0800
 Birth: -

使用 lseek 可以修改文件的当前读写位置偏移量,此函数不但可以改变位置偏移量,并且还允许文件偏移量超出文件长度,譬如有一个 test_file,该文件的大小是 4K(也就是 4096 个字节),通过 lseek 系统调用可以将该文件的读写偏移量移动到偏移文件头部 6000 个字节处。

接下来使用 write 函数对文件进行写入操作,也就是说此时将是从偏移文件头部 6000 个字节处开始写入数据,也就意味着 4096~6000 字节之间出现了一个空洞,因为这部分空间并没有写入任何数据,所以形成了空洞,这部分区域就被称为文件空洞,那么相应的该文件也被称为空洞文件。文件空洞部分实际上并不会占用任何物理空间,直到在某个时刻对空洞部分进行写入数据时才会为它分配对应的空间,但是空洞文件形成时,逻辑上该文件的大小是包含了空洞部分的大小的,这点需要注意。

空洞文件的作用

空洞文件的好处是:空洞文件对多线程共同操作文件是很有用的。

因为我们在创建一个很大文件的时候,我们就把一个文件分成很多的段,然后采用多线程的方式,让每个线程负责写入其中的某一段的数据。这样的话比我们用单个线程写入是快很多的。

例如:

在使用迅雷下载文件时,还未下载完成,就发现该文件已经占据了全部文件大小的空间,这也是空洞文件;下载时如果没有空洞文件,多线程下载时文件就只能从一个地方写入,这就不能发挥多线程的作用了;如果有了空洞文件,可以从不同的地址同时写入,就达到了多线程的优势;

在创建虚拟机时,你给虚拟机分配了 100G 的磁盘空间,但其实系统安装完成之后,开始也不过只用了 3、4G 的磁盘空间,如果一开始就把 100G 分配出去,资源是很大的浪费。

4.2.3 st_mode

① 简介

st_mode是一个16位的位图,用于表示文件类型,文件访问权限以及特殊权限位。

它的类型为mode_t,其实就是普通的unsigned int,但是只是用了低16位。

② 实例分析

假设st_mode表示为八进制的100664:

则有:

  • 1000: 这是一个常规文件
  • 000: 执行时设置信息为空,黏着位为 0
  • 110-110-100: 用户权限为 RW-,组员权限为RW-,其他人权限为R--
③ 宏

通过手工分析 st_mode 字段,实际上是很不方便的。实际写程序,可使用 st_mode & 掩码来得到 st_mode 中特定的部分。比如:

cs 复制代码
st_mode & 0170000 : 得到文件类型
st_mode & 0007000 : 得到执行文件时设置信息
st_mode & 0000777 : 得到权限位
st_mode & 00100: 判断所有者是否可执行
(以上数字均为八进制)

为了方便使用,用 linux 预定义的一些宏来代替这些生硬的数字。

cs 复制代码
//bit15 ~ bit12 , 文件类型属性区域
#define  S_IFMT      0170000     文件类型的位遮罩
#define  S_IFSOCK    0140000     socket
#define  S_IFLNK     0120000     符号链接(symbolic link)
#define  S_IFREG     0100000     一般文件
#define  S_IFBLK     0060000     区块装置(block device)
#define  S_IFDIR     0040000     目录
#define  S_IFCHR     0020000     字符装置(character device)
#define  S_IFIFO     0010000     先进先出(fifo)

#define S_ISSOCK(m) (((m) & S_IFMT) == S_IFSOCK)  //提供了一些宏函数来帮助用户执行&操作,是则返回1
#define S_ISLNK(m)  (((m) & S_IFMT) == S_IFLNK)  
#define S_ISREG(m)  (((m) & S_IFMT) == S_IFREG)
#define S_ISBLK(m)  (((m) & S_IFMT) == S_IFBLK)
#define S_ISDIR(m)  (((m) & S_IFMT) == S_IFDIR)
#define S_ISCHR(m)  (((m) & S_IFMT) == S_IFCHR)
#define S_ISFIFO(m) (((m) & S_IFMT) == S_IFIFO)

//bit11 ~ bit9,权限的特殊属性区域
#define  S_ISUID      0004000     文件的(set user-id on execution)位
#define  S_ISGID      0002000     文件的(set group-id on execution)位
#define  S_ISVTX      0001000     文件的sticky位

//bit8 ~ bit0,权限属性区域
//文件所有者(owner)
#define S_IRWXU 00700	/* mask for file owner permissions */
#define S_IRUSR 00400	/* owner has read permission */
#define S_IWUSR 00200	/* owner has write permission */
#define S_IXUSR 00100	/* owner has execute permission */
 //组用户(group)
#define S_IRWXG 00070	/* mask for group permissions */
#define S_IRGRP 00040	/* group has read permission */
#define S_IWGRP 00020	/* group has write permission */
#define S_IXGRP 00010	/* group has execute permission */
 //其他用户(other)
#define S_IRWXO 00007	/* mask for permissions for others (not in group) */
#define S_IROTH 00004	/* others have read permission */
#define S_IWOTH 00002	/* others have write permission */
#define S_IXOTH 00001	/* others have execute permission */
④ 代码示例
cs 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>

static int ftype(const char *fname) {
    struct stat statres;

    if(stat(fname, &statres) < 0) {
        perror("stat()");
        exit(1);
    }

    if(S_ISREG(statres.st_mode))
        return '-';
    else if(S_ISDIR(statres.st_mode))
        return 'd';
    else if(S_ISSOCK(statres.st_mode))
        return 's';
    else
        return '?';
}

static int fper(const char *fname) {
    struct stat statres;

    if(stat(fname, &statres)) {
        perror("fper()");
        exit(1);
    }

    return statres.st_mode & S_IRWXU;
}


int main(int argc, char **argv) {
    if(argc < 2) {
        fprintf(stderr, "Usage...\n");
        exit(1);
    }

    printf("File type: %c\n", ftype(argv[1]));
	printf("Permission of owner: %o\n", fper(argv[1]));
    exit(0);
}

执行结果:

cs 复制代码
[root@zoukangcheng fs]# ./ftype ftype.c 
File type: -
Permission of owner: 600
[root@zoukangcheng fs]# ./ftype ftype
File type: -
Permission of owner: 700

4.2.4 文件权限

① umask

umask函数原型:

cs 复制代码
#include <sys/stat.h>
mode_t umask(mode_t mask);

在进程创建一个新的文件或目录时,如调用open函数创建一个新文件,新文件的实际存取权限是mode与umask按照 mode & ~umask运算以后的结果。umask函数用来修改进程的umask,作用是防止出现权限过松的文件。

② chmod

补充:chmod命令

语法:

cs 复制代码
chmod [对谁操作(ugoa)] [操作符 (+-=)] [赋予的权限(rwxs或数字)] 文件名1 文件名2...

例如:

cs 复制代码
[root@zoukangcheng fs]# ll big.c
-rw-r--r-- 1 root root 420 Dec  5 16:19 big.c
[root@zoukangcheng fs]# chmod 664 big.c
[root@zoukangcheng fs]# ll big.c
-rw-rw-r-- 1 root root 420 Dec  5 16:19 big.c

chmod函数原型:

cs 复制代码
#include <sys/stat.h>
int chmod(const char *pathname, mode_t mode);
int fchmod(int fd, mode_t mode);

chmod是对指定的文件进行操作,而fchmod则是对已经打开的文件进行操作。所以它们的第一个参数不一样。

返回值:如果改变成功返回0,否则返回-1

代码示例

cs 复制代码
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
 
int main()
{
    int fd = open("a", O_RDWR);
    if (-1 == fd)
    {
       perror("open fail");
       exit(1);
    }
 	// 将fd的权限更改为0777
    if(-1 == fchmod(fd, 0777))
    {
        perror("fchmod fail");
        exit(1);
    }
 	
    // 将b文件的权限更改为0777
    if (-1 == chmod("b", 0777))
    {
       perror("fchmod fail");
       exit(1);
    }
    
    close(fd);
    return 0;
}
③ 粘住位

在UNIX尚未使用分页技术的早期版本中,S_ISVTX位被称为粘住位(sticky bit)。

如果一个可执行程序文件的这一位被设置了,那么在该程序第一次被执行并结束时,其程序正文部分的一个副本仍被保存在交换区,(程序的正文部分是机器指令部分)。这使得下次执行该程序时能较快地将其装入内存中。其原因是:交换区占用连续磁盘空间,可将它视为连续文件,而且一个程序的正文部分在交换区中也是连续存放的,而在一般的UNIX文件系统中,文件的各数据块很可能是随机存放的。

对于常用的应用程序,例如文本编辑器和C编译器,我们常常设置它们所在文件的粘住位。自然,对于在交换区中可以同时存放的设置了粘住位的文件数是有一定限制的,以免过多占用交换区空间,但无论如何这是一个有用的技术。后来的UNIX版本称它为保存正文位(saved-text bit),因此也就有了常量S_ISVTX。现今较新的UNIX系统大多数都配置有虚拟存储系统以及快速文件系统,所以不再需要使用这种技术。

现今的系统扩展了粘住位的使用范围,允许针对目录设置粘住位。如果对一个目录设置了粘住位,则只有对该目录具有写权限的用户在满足下列之一的情况下,才能删除或更名该目录下的文件或目录:

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

目录/tmp是设置粘住位的典型候选者:任何用户都可在这个目录中创建文件。任一用户(用户、组和其他)对这个目录的权限通常都是读、写和执行(rwx)。但是用户不应能删除或更名属于其他人的文件,为此在这个目录的文件模式中都设置了粘住位。

即:假如/tmp下的文件A被用户U1所有,文件A的权限为777,那么所有用户都可以对该文件进行修改、移动、重命名等操作,但无法删除该文件 。通常的用途在于用户团队协作的目录,用户可以相互修改文件,却只有用户所有者才能删除。

图中最后一位t就是粘住位。

4.2.5 文件系统

① 磁盘的结构

磁盘

一个磁盘(如一个 1T 的机械硬盘)由多个盘片(如下图中的 0 号盘片)叠加而成。

盘片的表面涂有磁性物质,这些磁性物质用来记录二进制数据。因为正反两面都可涂上磁性物质,故一个盘片可能会有两个盘面。

磁道和扇区

每个盘片被划分为一个个磁道(一个一个半径不同的同心圆环),每个磁道又划分为一个个扇区(磁道上的一个弧段)。扇区是磁盘的最小组成单元,通常是512字节。如下图:

其中,最内侧磁道上的扇区面积最小,因此数据密度最大。

柱面

每个盘面对应一个磁头。所有的磁头都是连在同一个磁臂上的,因此所有磁头只能"共进退"。

所有盘面中半径相同的磁道组成柱面。如下图:

磁盘容量计算

存储容量 = 磁头数 × 磁道(柱面)数 × 每道扇区数 × 每扇区字节数

磁盘的物理地址

由上,可用(柱面号,盘面号,扇区号)来定位任意一个"磁盘块",这里的"磁盘块",实质上就是一个扇区。

可根据该地址读取一个"块",操作如下:

  1. 根据"柱面号"前后移动磁臂,让磁头指向指定柱面;
  2. 旋转磁盘,让磁头抵达待读的起始扇区。
  3. 激活指定盘面对应的磁头;
  4. 旋转磁盘,指定的扇区会从磁头下面划过,这样就完成了对指定扇区的读/写。

块和簇

磁盘块/簇(虚拟出来的)。 块是操作系统中最小的逻辑存储单位。操作系统与磁盘打交道的最小单位是磁盘块。

在Windows下如NTFS等文件系统中叫做簇;

在Linux下如Ext4等文件系统中叫做块(block)。一般来说,一个块(block)包含连续的8个扇区,每个扇区512B,因此一个块大小为4096KB。

每个簇或者块可以包括2、4、8、16、32、64...2的n次方个扇区。

② 文件系统简介

文件系统:文件或数据的存储和管理。目前,正在使用的UNIX文件系统有多种实现。

  • 传统的基于BSD的UNIX文件系统(称为UFS)。UFS是以Berkeley快速文件系统为基础的。本节讨论该文件系统。
  • 读、写DOS格式软盘的文件系统(称为PCFS)
  • 读CD的文件系统(称为HSFS)

我们可以把一个磁盘分成一个或多个分区。每个分区可以包含一个文件系统 。

  • i节点即为inode结构体的数组
  • 数据块一般被分成了大小为4KB的块(block)
  • i节点图(i节点位图):用来判断inode的空闲与占用情况
  • 块位图:用来判断数据块的占用与空闲情况

对于普通文件,如下图:

  • i节点结构体中通常包含15个指针来指向数据块,最后三个指针为一二三级指针,用于扩充文件的大小;图中的i节点指向了三个数据块。
  • 在图中有两个目录项(两个不同文件名的文件,但是inode编号相同)指向同一个i节点(此时称为硬链接 ,即目录项就是硬链接的同义词 )。每个i节点中都有一个硬链接计数st_nlink,其值是指向该i节点的目录项数。只有当链接计数减少至0时,才可删除该文件(也就是可以释放该文件占用的数据块)。
  • i节点包含了文件有关的所有信息∶文件类型、文件访问权限位、文件长度和指向文件数据块的指针等。stat 结构中的大多数信息都取自i节点。只有两项重要数据存放在目录项中∶文件名和i节点编号。

对于目录,目录也是一种文件,它的属性也需要inode结构体存储,它的物理存储也需要通过inode中的指针来指向的数据块(此时的数据块就是目录块)来存储;

目录块存储的内容非常的简单,由目录项 组成,每条目录项有包含的文件名以及该文件名对应的inode编号。

如下图所示:

  • 编号为2549的i节点(testdir),其类型字段st_mode表示它是一个目录(因此它指向一个特殊的数据块------目录块),链接计数为2。任何一个叶目录(不包含任何其他目录的目录)的链接计数总是2,数值2来自于命名该目录(testdir)的目录项以及在该目录中的.项。
  • 编号为1267的i节点,其类型字段st_mode表示它是一个目录,链接计数大于或等于3。它大于或等于3的原因是,至少有3个目录项指向它∶一个是命名它的目录项,第二个是在该目录中的.项,第三个是在其子目录 testdir 中的..项。注意,在父目录中的每一个子目录都使该父目录的链接计数增加1。
③ 链接

链接分为硬链接和符号链接(注意不是软链接)。

创建链接的命令:

cs 复制代码
ln src dest # 创建src的硬链接为dest
cs 复制代码
ln -s src dest # 创建src的符号链接为dest

硬链接对比:

cs 复制代码
[root@HongyiZeng tmp]# ln bigfile bigfile_link
[root@HongyiZeng tmp]# stat bigfile
  File: 'bigfile'
  Size: 5368709120      Blocks: 8          IO Block: 4096   regular file
Device: fd01h/64769d    Inode: 26199       Links: 2
Access: (0600/-rw-------)  Uid: (    0/    root)   Gid: (    0/    root)
Access: 2022-12-05 16:26:39.166784814 +0800
Modify: 2022-12-05 16:19:40.734216438 +0800
Change: 2022-12-05 20:29:33.176048702 +0800
 Birth: -
[root@HongyiZeng tmp]# stat bigfile_link 
  File: 'bigfile_link'
  Size: 5368709120      Blocks: 8          IO Block: 4096   regular file
Device: fd01h/64769d    Inode: 26199       Links: 2
Access: (0600/-rw-------)  Uid: (    0/    root)   Gid: (    0/    root)
Access: 2022-12-05 16:26:39.166784814 +0800
Modify: 2022-12-05 16:19:40.734216438 +0800
Change: 2022-12-05 20:29:33.176048702 +0800
 Birth: -

字段Link即为文件的硬链接数,硬链接数为2,说明有两个目录项指向了这个inode,并且注意两个文件的inode编号相同,说明同时指向了这个inode;

将源文件bigfile删除后,bigfile_link仍然存在:

cs 复制代码
[root@HongyiZeng tmp]# rm -rf bigfile
[root@HongyiZeng tmp]# stat bigfile_link 
  File: 'bigfile_link'
  Size: 5368709120      Blocks: 8          IO Block: 4096   regular file
Device: fd01h/64769d    Inode: 26199       Links: 1
Access: (0600/-rw-------)  Uid: (    0/    root)   Gid: (    0/    root)
Access: 2022-12-05 16:26:39.166784814 +0800
Modify: 2022-12-05 16:19:40.734216438 +0800
Change: 2022-12-05 20:33:03.459331364 +0800
 Birth: -

此时硬链接数Link变为1;


符号链接对比:

cs 复制代码
[root@HongyiZeng tmp]# ln -s bigfile_link bigfile_s
[root@HongyiZeng tmp]# stat bigfile_link 
  File: 'bigfile_link'
  Size: 5368709120      Blocks: 8          IO Block: 4096   regular file
Device: fd01h/64769d    Inode: 26199       Links: 1
Access: (0600/-rw-------)  Uid: (    0/    root)   Gid: (    0/    root)
Access: 2022-12-05 16:26:39.166784814 +0800
Modify: 2022-12-05 16:19:40.734216438 +0800
Change: 2022-12-05 20:33:03.459331364 +0800
 Birth: -
[root@HongyiZeng tmp]# stat bigfile_s
  File: 'bigfile_s' -> 'bigfile_link'
  Size: 12              Blocks: 0          IO Block: 4096   symbolic link
Device: fd01h/64769d    Inode: 26201       Links: 1
Access: (0777/lrwxrwxrwx)  Uid: (    0/    root)   Gid: (    0/    root)
Access: 2022-12-05 20:34:11.189456100 +0800
Modify: 2022-12-05 20:34:11.189456100 +0800
Change: 2022-12-05 20:34:11.189456100 +0800
 Birth: -
[root@HongyiZeng tmp]# ll bigfile_s
lrwxrwxrwx 1 root root 12 Dec  5 20:34 bigfile_s -> bigfile_link

可以看到硬链接数并未改变;此外符号链接文件的大小为12字节,物理占用的块甚至为0;文件类型标识也变为了symbolic link和l;

删除原始文件后,发现符号链接文件的链接已成非法链接(红色部分):


相关的系统调用:

link 函数专门用来创建硬链接的,功能和 ln 命令一样。

cs 复制代码
#include <unistd.h>
int link(const char *oldpath, const char *newpath);

unlink函数删除一个文件的目录项并减少它的链接数,若成功则返回0,否则返回-1,错误原因存于error。

cs 复制代码
#include <unistd.h>
int unlink(const char *pathname)

对比:硬链接不能给分区建立,不能给目录建立,而符号链接可以。

4.2.6 杂项

文件的删除、重命名和移动

remove

cs 复制代码
#include <stdio.h>
int remove(const char *pathname);

rename:是mv命令的系统调用

cs 复制代码
#include <stdio.h>
int rename(const char *oldpath, const char *newpath);

更改文件的时间

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

cs 复制代码
#include <sys/types.h>
#include <utime.h>
int utime(const char *filename, const struct utimbuf *times);

更改当前工作路径

chdir:是cd命令的系统调用

cs 复制代码
#include <unistd.h>
int chdir(const char *path);
int fchdir(int fd);

getcwd:是pwd命令的系统调用

cs 复制代码
#include <unistd.h>
char *getcwd(char *buf, size_t size);
char *getwd(char *buf);
char *get_current_dir_name(void);

4.2.7 glob

该节内容为分析目录和读取目录内容。

glob函数原型(模型匹配函数):

cs 复制代码
#include <glob.h>
int glob(const char *pattern, int flags,
         int (*errfunc) (const char *epath, int eerrno),
         glob_t *pglob);
// 释放glob函数调用的空间
void globfree(glob_t *pglob);

pattern: 通配符,要分析的pattern,如/*表示匹配根文件下的所有文件(不包括隐藏文件)

flags:flags参数可以设置特殊要求,如无特殊要求置为0

errfunc:函数指针,glob函数执行出错会执行的函数,出错的路径会回填到epath中,出错的原因回填到eerrno中。如不关注错误可设置为NULL

pglob:解析出来的结果放在这个参数里,是一个结构体指针

返回值:成功返回0,错误返回非0

其中,glob_t是一个结构体:

cs 复制代码
typedef struct {
    // pathc与pathv类似于main函数参数argc与argv
    size_t    gl_pathc;    //匹配到的数量
    char    **gl_pathv;    //匹配到的元素放在这里
    size_t    gl_offs; 
} glob_t;

程序实例

打印出/etc/*.conf的文件名。

cs 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <glob.h>
// 通配符
#define PAT "//etc//*.conf"

static int errfunc(const char *epath, int eerrno) {
    puts(epath);
    fprintf(stderr, "ERROR: %d", eerrno);
    return 1;
}

int main() {
    glob_t globres;
    int err;
    err = glob(PAT, 0, errfunc, &globres);

    if(err) {
        printf("ERROR CODE = %d\n", err);
        exit(1);
    }

    int i;

    for(i = 0; i < globres.gl_pathc; i++) {
        puts(globres.gl_pathv[i]);
    }
    // 释放空间
	globfree(&globres);
    exit(0);
}

执行结果:

cs 复制代码
[root@zoukangcheng fs]# ./glob 
/etc/GeoIP.conf
/etc/asound.conf
/etc/chrony.conf
/etc/dat.conf
其余略

4.2.8 目录函数

opendir

closedir

readdir

一组对目录操作的函数

和对文件操作的函数基于FILE结构体类似,对目录的操作基于名为DIR的结构体。

下面是常用的对目录进行操作的函数,他们的功能可以被glob替代。

4.2.9 目录解析实现

需求:实现du的功能,统计文件或目录的字节大小

代码示例

cs 复制代码
#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;
}

执行结果:

4.3 用户信息和函数

4.3.1 /etc/passwd

获取文件入口密码

getpwuid();

getpwnam();

返回值为一个结构体passwd

4.3.2 /etc/group

获取组文件入口

getgrgid()

getgrgrnam()

返回值为一个结构体

4.3.3 /etc/shadow

getspnam();

crypt();密码加密

getpass();

密码校验实例

cs 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <shadow.h>
#include <string.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]);

    crypted_passwd = crypt(input_passwd,shadowline->sp_pwdp);
    
    if (strcmp(shadowline->sp_pwdp,crypted_passwd) == 0)
      puts("ok!");
    else
      puts("failed!");

    return 0;
}

执行结果:

此时需要我们切换到root用户

4.3.3 时间函数

时间戳: time_t char * struct tm

常用时间函数:

time(), gmtime(),localtime(),mktime(),strftime();

gmtime和localtime返回值是一个指向结构体tm的指针

实例:

需求:一秒一个时间戳,当程序终止,停止书写时间戳,当程序再次被打开,在此前基础上继续书写时间戳

cs 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <time.h>
#include <string.h>

#define BUFSIZE 1024

int main()
{

    char fmttime[BUFSIZ];
    int count = 0;

    FILE *fptr = fopen("./log","a+");
    if (fptr == NULL) {
        perror("fopen()");
        exit(1);
    }

    char buf[BUFSIZE];

    while(fgets(buf,BUFSIZE,fptr) != NULL){
        count++;
    }

    char res[BUFSIZE];

    while (1){
        time_t stamp;
        stamp = time(NULL);

        struct tm *struct_tm;
        struct_tm = localtime(&stamp);

        strftime(fmttime,BUFSIZE,"%Y-%m-%d %H:%M:%S",struct_tm);
        fprintf(fptr,"%d %s\n",++count,fmttime);
        fflush(fptr);
        sleep(1);
    }

    fclose(fptr);

    exit(0);
}

4.4 进程环境

本节对应第七章------进程环境;

4.4.1 main函数

C程序总是从main函数开始执行,从main函数结束执行。即main是程序的入口和出口。

当内核执行C程序时(使用一个exec函数),在调用main前先调用一个特殊的启动例程。可执行程序文件将此启动例程指定为程序的起始地址---------这是由连接编辑器设置的,而连接编辑器则由C编译器调用。

启动例程从内核取得命令行参数和环境变量值,然后为按上述方式调用main函数做好安排。

4.4.2 进程终止

4.4.2.1 终止方式

共有8种方式让进程终止。其中5种为正常退出:

1 从main返回

2 调用exit(C库函数)

3 调用_exit或_Exit(系统调用)

4 最后一个线程从其启动例程返回

5 从最后一个线程调用pthread_exit

异常终止有3种方式:

1 调用abort

2 接到一个信号

3 最后一个线程对取消请求做出响应

4.4.2.2 main函数的返回值

main函数的返回值给main函数的父进程。

假设一个程序名为main_test,则在终端上执行该程序时,父进程为shell:

cs 复制代码
./main_test

可通过$?(表示显示最后命令的退出状态)查看该返回值。

cs 复制代码
#include <stdio.h>
#include <stdlib.h>

int main() {
    printf("Hello World!\n");
    return 0;
}

执行结果:

cs 复制代码
[root@zoukangcheng fs]# ./main1 
Hello World!
[root@zoukangcheng fs]# echo $?
0

如果将程序更改为:

cs 复制代码
#include <stdio.h>
#include <stdlib.h>

int main() {
    printf("Hello World!\n");
}

则执行结果为:

cs 复制代码
[root@zoukangcheng fs]# ./main1 
Hello World!
[root@zoukangcheng fs]# echo $?
13

此时会将printf的返回值(打印字符的个数)作为main函数的返回值给父进程shell。

4.4.2.3 钩子函数ateixt,on_exit

按照ISO C的规定,一个进程可以登记多至32个函数,这些函数将由exit自动调用。我们称这些函数为终止处理程序(exit handler),并调用 atexit 函数来登记这些函数。

cs 复制代码
int atexit(void (*func)(void));

atexit当程序正常终止时,调用指定的函数(终止处理程序) func。可以在任何地方注册终止函数,但它会在程序终止的时候被调用。先注册的后调用。

func --- 在程序终止时被调用的函数,该函数无参且无返回值,它是一个函数指针,因此传入的参数应该是一个函数的地址,即函数名(函数名就是函数的首地址)。

如果函数成功注册,则该函数返回零,否则返回一个非零值。

程序实例

cs 复制代码
#include <stdio.h>
#include <stdlib.h>

// 终止处理程序
static void f1() {
    puts("f1() is working!");
}

// 终止处理程序
static void f2() {
    puts("f2() is working!");
}

// 终止处理程序
static void f3() {
    puts("f3() is working!");
}

int main() {
    puts("Begin");
    // 先注册的后被调用
    // 钩子函数的书写顺序并不是实际执行顺序,atexit会在程序终止时被调用
    // atexit参数用指针来接收,因此需要传入地址,而函数名就是函数的地址
    atexit(f1);
    atexit(f2);
    atexit(f3);

    puts("End");
    exit(0);
}

执行结果:

cs 复制代码
Begin
End
f3() is working!
f2() is working!
f1() is working!
4.4.2.4 exit,_exit

exit是库函数,而_exit是系统调用,前者使用了后者。

除此之外,_exit()执行后会立即返回给内核,而exit()要先执行一些清除和终止操作,然后将控制权交给内核。

4.4.3 命令行参数

getopt()

getopt_long()

程序实例

需求:实现ls命令行

cs 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <glob.h>
#include <string.h>
#include <dirent.h>

#include "myls.h"

#define PAT "/etc/a*.conf"

static struct stat statres;

//文件大小
static off_t flen(const char *fname){
    if (stat(fname,&statres) < 0) {
        perror("stat()");
        exit(1);
    }
    return statres.st_size;
}

//文件类型
static char ftype(const char* fname) {
    if (stat(fname,&statres) < 0) {
        perror("stat()");
        exit(1);
    }
    if (S_ISREG(statres.st_mode)) {
        return '-';
    }else if (S_ISDIR(statres.st_mode)) {
        return 'd';
    }else{
        return '?';
    }
}

//解析失败处理函数
static int errfunc (const char *errpath,int eerrno) {
    puts(errpath);
    fprintf(stderr,"ERROR MSG: %s\n",strerror(eerrno));
    return 0;
}


//glob解析路径
static void Glob(){
    glob_t globres;
    int err = glob(PAT,0,&errfunc,&globres);
    if (err) {
        printf("Error code = %d\n",err);
    }

    for (int i = 0;globres.gl_pathv[i]!= NULL;i++) {
        fprintf(stdout,"%s\n",globres.gl_pathv[i]);
    }

}
/**
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
  };
*/
#define VNAME(value) (#value)
//组合解析路径
static void PathParse(char *Path) {
    DIR *dp;
    struct dirent *cur;

    dp = opendir(Path);
    if (dp == NULL) {
          perror("opendir");
          exit(1);
    }

    while((cur = readdir(dp)) != NULL) {
        fprintf(stdout,"%s type:%d inode:%ld\n",cur->d_name,cur->d_type,cur->d_ino);
    }

    closedir(dp);
}

int main(int argc,char **argv)
{
    if (argc < 2) {
        fprintf(stderr,"Usage...\n");
        exit(1);
    }

    int c = 0;
    fileAttr f;
    char fmtstr[FMTSTRSIZE];
    fmtstr[0] = 0;
    

    //解析命令行
    while(1) {
        c = getopt(argc,argv,"lt-a"); // - 识别非选项的传参
        if (c < 0){
            break;
        }
        
        switch (c){
            case 'l':
                f.filesize = flen(argv[1]);
                strncat(fmtstr,"filesize:%ld ",FMTSTRSIZE-1);
                break;
            case 't':
                f.filetype = ftype(argv[1]);
                strncat(fmtstr,"filetype:%c ",FMTSTRSIZE-1);
                break;
            case 'a':
                PathParse(argv[optind]);
                break;
            default:
                break;
        }
    }

    printf(fmtstr,f.filesize,f.filetype);
    //char pwd[1024];
    //getcwd(pwd,1024);
    //fprintf(stdout,"%s\n",pwd);
    //Glob();
    //PathParse(pwd);

    exit(0);
}

4.4.4 环境变量

4.4.4.1 简介

环境变量的含义:程序(操作系统命令和应用程序)的执行都需要运行环境,这个环境是由多个环境变量组成的。

按变量的周期划为永久变量和临时性变量2种:

永久变量:通过修改配置文件,配置之后变量永久生效。

临时性变量:使用命令如export等命令设置,设置之后马上生效。当关闭shell的时候失效(这种主要用于测试比较多)。

按照影响范围分为用户变量和系统变量2种:

用户变量(局部变量):修改的设置只对某个用户的路径或执行起作用;

系统变量(全局变量):影响范围是整个系统;

环境变量本质上是一个kv键值对。

4.4.4.2 查看环境变量

在Shell下,用env命令查看当前用户全部的环境变量。

cs 复制代码
[root@zoukangcheng fs]# env
XDG_SESSION_ID=139874
HOSTNAME=HongyiZeng
TERM=xterm-256color
SHELL=/bin/bash
HISTSIZE=3000
SSH_TTY=/dev/pts/0
USER=root
MAIL=/var/spool/mail/root
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/usr/local/java/jdk1.8.0_161/bin:/root/bin:/root/bin
PWD=/usr/local/linux_c/fs
JAVA_HOME=/usr/local/java/jdk1.8.0_161
LANG=en_US.UTF-8
NEXUS_HOME=/usr/local/nexus/nexus-3.20.1-01
SHLVL=1
HOME=/root
LOGNAME=root
CLASSPATH=/usr/local/java/jdk1.8.0_161/lib/
SSH_CONNECTION=81.69.102.136 35390 10.0.24.5 22
LESSOPEN=||/usr/bin/lesspipe.sh %s
PROMPT_COMMAND=history -a; history -a; printf "\033]0;%s@%s:%s\007" "${USER}" "${HOSTNAME%%.*}" "${PWD/#$HOME/~}"
XDG_RUNTIME_DIR=/run/user/0
HISTTIMEFORMAT=%F %T 
_=/usr/bin/env
OLDPWD=/usr/local/linux_c

export命令显示当前系统定义的所有环境变量。

cs 复制代码
[root@zoukangcheng fs]# export
declare -x CLASSPATH="/usr/local/java/jdk1.8.0_161/lib/"
declare -x HISTSIZE="3000"
declare -x HISTTIMEFORMAT="%F %T "
declare -x HOME="/root"
declare -x HOSTNAME="HongyiZeng"
declare -x JAVA_HOME="/usr/local/java/jdk1.8.0_161"
declare -x LANG="en_US.UTF-8"
declare -x LESSOPEN="||/usr/bin/lesspipe.sh %s"
declare -x LOGNAME="root"
declare -x MAIL="/var/spool/mail/root"
declare -x NEXUS_HOME="/usr/local/nexus/nexus-3.20.1-01"
declare -x OLDPWD="/usr/local/linux_c"
declare -x PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/usr/local/java/jdk1.8.0_161/bin:/root/bin:/root/bin"
declare -x PROMPT_COMMAND="history -a; history -a; printf \"\\033]0;%s@%s:%s\\007\" \"\${USER}\" \"\${HOSTNAME%%.*}\" \"\${PWD/#\$HOME/~}\""
declare -x PWD="/usr/local/linux_c/fs"
declare -x SHELL="/bin/bash"
declare -x SHLVL="1"
declare -x SSH_CLIENT="81.69.102.136 35390 22"
declare -x SSH_CONNECTION="81.69.102.136 35390 10.0.24.5 22"
declare -x SSH_TTY="/dev/pts/0"
declare -x TERM="xterm-256color"
declare -x USER="root"
declare -x XDG_RUNTIME_DIR="/run/user/0"
declare -x XDG_SESSION_ID="139874"

查看某个环境变量的值:

cs 复制代码
echo $KEY

例如,查看PATH的值:

cs 复制代码
[root@zoukangcheng fs]# echo $PATH
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/usr/local/java/jdk1.8.0_161/bin:/root/bin:/root/bin
4.4.4.3 设置环境变量

在用户的家目录/home/用户名下,有几个特别的文件:

.bash_profile(推荐首选):当用户登录时执行,每个用户都可以使用该文件来配置专属于自己的环境变量。

.bashrc:当用户登录时以及每次打开新的Shell时该文件都将被读取,不推荐在里面配置用户专用的环境变量,因为每开一个Shell,该文件都会被读取一次,效率肯定受影响。

.bash_logout:当每次退出系统(退出bash shell)时执行该文件。

.bash_history:保存了当前用户使用过的历史命令

4.4.4.4 环境表

每个程序都接收到一张环境表。与参数表一样,环境表也是一个字符指针数组,其中每个指针包含一个以null结束的C字符串的地址。

全局变量environ则包含了该指针数组的地址∶

cs 复制代码
extern char **environ;

例如,如果该环境包含5个字符串,那么它看起来如图中所示。其中,每个字符串的结尾处都显式地有一个null字节。我们称 environ 为环境指针,指针数组为环境表,其中各指针指向的字符串为环境字符串。

代码示例

cs 复制代码
#include <stdio.h>
#include <stdlib.h>

// 引用声明外部的变量
extern char **environ;

int main(void) {
    int i;
    for(i = 0; environ[i] != NULL; i++) {
        puts(environ[i]);
    }
    exit(0);
}

相关库函数:

cs 复制代码
#include <stdlib.h>

// 根据环境变量的键name来获取环境变量的值
char *getenv(const char *name);
cs 复制代码
#include <stdlib.h>

// 键name,值value,overwrite是否覆盖
// 当name存在时,且overwrite为1,表示改变环境变量
int setenv(const char *name, const char *value, int overwrite);

// 根据key删除环境变量
int unsetenv(const char *name);

程序实例

cs 复制代码
#include <stdio.h>
#include <stdlib.h>

int main(void) {
    puts(getenv("PATH"));
    exit(0);
}

4.4.5 共享库

共享库使得可执行文件中不再需要包含公用的库函数,而只需在所有进程都可引用的存储区中保存这种库例程的一个副本。

程序第一次执行或者第一次调用某个库函数时,用动态链接方法将程序与共享库函数相链接。这减少了每个可执行文件的长度,但增加了一些运行时间开销。这种时间开销发生在该程序第一次被执行时,或者每个共享库函数第一次被调用时。

共享库的另一个优点是可以用库函数的新版本代替老版本而无需对使用该库的程序重新连接编辑(假定参数的数目和类型都没有发生改变)。

动态库的相关库函数

cs 复制代码
// dladdr,  dlclose,  dlerror,  dlopen,  dlsym, dlvsym - programming interface to dynamic linking loader
#include <dlfcn.h>
// 该函数将打开一个新库,并把它装入内存。该函数主要用来加载库中的符号,这些符号在编译的时候是不知道的。这种机制使得在系统中添加或者删除一个模块时,都不需要重新进行编译
void *dlopen(const char *filename, int flag);

// 返回一个描述最后一次调用dlopen、dlsym,或dlclose的错误信息的字符串
char *dlerror(void);

// 在打开的动态库中查找符号的值
void *dlsym(void *handle, const char *symbol);

// 关闭动态库
int dlclose(void *handle);

4.4.6 函数间跳转

补充:goto语句

C 语言中的 goto 语句允许把控制无条件转移到同一函数内的被标记的语句。

语法:

cs 复制代码
goto label;
..
.
label: statement;

在这里,label 可以是任何除 C 关键字以外的纯文本,它可以设置在 C 程序中 goto 语句的前面或者后面

setjmp和longjmp 可以实现非局部控制转移,即从一个函数到到另外一个函数的跳转

函数原型:

cs 复制代码
#include <setjmp.h>
int setjmp(jmp_buf buf);
void longjmp(jmp_buf env, int val);

setjmp函数用于记录当前位置,保存调用函数的栈环境在结构体jmp_buf buf(相当于保护现场)。函数输入参数为jmp_buf类型(这个结构体类似于goto的跳转标识),返回整型。当第一次调用时(设置跳转点),它的值为0;当第二次调用时(从别处跳转回来,即调用longjmp时)返回非零值;总之执行一次,返回两次,因此,setjmp函数后常常跟上分支语句。

longjmp的作用是使用setjmp保存在buf中的栈环境信息返回到setjmp的位置,也就是当执行longjmp时程序又回到setjmp处(相当于恢复现场)。形参val是调用longjmp时setjmp函数返回的值,为非零值,如果故意设置为0,也会被修改为1;

程序实例

cs 复制代码
#include <stdio.h>
#include <stdlib.h>

static void d(void) {
    printf("%s():Begin.\n", __FUNCTION__);
    printf("%s():End.\n", __FUNCTION__);
}

static void c(void) {
    printf("%s():Begin.\n", __FUNCTION__);
    printf("%s():Call d().\n", __FUNCTION__);
    d();
    printf("%s():d() returned.\n", __FUNCTION__);
    printf("%s():End.\n", __FUNCTION__);
}


static void b(void) {
    printf("%s():Begin.\n", __FUNCTION__);
    printf("%s():Call c().\n", __FUNCTION__);
    c();
    printf("%s():c() returned.\n", __FUNCTION__);
    printf("%s():End.\n", __FUNCTION__);
}


static void a(void) {
    printf("%s():Begin.\n", __FUNCTION__);
    printf("%s():Call b().\n", __FUNCTION__);
    b();
    printf("%s():b() returned.\n", __FUNCTION__);
    printf("%s():End.\n", __FUNCTION__);
}

int main(void) {

    printf("%s():Begin.\n", __FUNCTION__);
    printf("%s():Call a().\n", __FUNCTION__);
    a();
    printf("%s():a() returned.\n", __FUNCTION__);
    printf("%s():End.\n", __FUNCTION__);
    exit(0);
}

执行结果:

cs 复制代码
main():Begin.
main():Call a().
a():Begin.
a():Call b().
b():Begin.
b():Call c().
c():Begin.
c():Call d().
d():Begin.
d():End.
c():d() returned.
c():End.
b():c() returned.
b():End.
a():b() returned.
a():End.
main():a() returned.
main():End.

注:ANSI C 定义了许多宏。在编程中可以使用这些宏,但是不能直接修改这些预定义的宏。

例如:

cs 复制代码
__DATE__ 当前日期,一个以 "MMM DD YYYY" 格式表示的字符串常量。
__TIME__ 当前时间,一个以 "HH:MM:SS" 格式表示的字符串常量。
__FILE__ 这会包含当前文件名,一个字符串常量。
__LINE__ 这会包含当前行号,一个十进制常量。
__FUNCTION__ 程序预编译时预编译器将用所在的函数名,返回值是字符串;

现在改写程序,在函数a进行setjmp,函数d进行longjmp:

cs 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <setjmp.h>

// 跳转点的现场环境
static jmp_buf save;

static void d(void) {
    printf("%s():Begin.\n", __FUNCTION__);
    printf("%s():Jump now!.\n", __FUNCTION__);
    // 向save跳转,并携带返回值为6
    longjmp(save, 6);
    printf("%s():End.\n", __FUNCTION__);
}

static void c(void) {
    printf("%s():Begin.\n", __FUNCTION__);
    printf("%s():Call d().\n", __FUNCTION__);
    d();
    printf("%s():d() returned.\n", __FUNCTION__);
    printf("%s():End.\n", __FUNCTION__);
}


static void b(void) {
    printf("%s():Begin.\n", __FUNCTION__);
    printf("%s():Call c().\n", __FUNCTION__);
    c();
    printf("%s():c() returned.\n", __FUNCTION__);
    printf("%s():End.\n", __FUNCTION__);
}


static void a(void) {
    // 返回值
    int ret;
    printf("%s():Begin.\n", __FUNCTION__);
    // 设置跳转点
    // setjmp一次调用,两次返回
    ret = setjmp(save);
    if(ret == 0) {
        printf("%s():Call b().\n", __FUNCTION__);
        b();
        printf("%s():b() returned.\n", __FUNCTION__);
    } else {
        printf("%s():Jumped back here with code %d.\n", __FUNCTION__, ret);
    }
    printf("%s():End.\n", __FUNCTION__);
}

int main(void) {

    printf("%s():Begin.\n", __FUNCTION__);
    printf("%s():Call a().\n", __FUNCTION__);
    a();
    printf("%s():a() returned.\n", __FUNCTION__);
    printf("%s():End.\n", __FUNCTION__);
    exit(0);
}

执行结果:

cs 复制代码
main():Begin.
main():Call a().
a():Begin.
a():Call b().
b():Begin.
b():Call c().
c():Begin.
c():Call d().
d():Begin.
d():Jump now!.
a():Jumped back here with code 6.
a():End.
main():a() returned.
main():End.

4.4.7 资源的获取和控制

获取或设置资源使用限制:linux下每种资源都有相关的软硬限制,软限制是内核强加给相应资源的限制值,硬限制是软限制的最大值。

非授权调用的进程只能将其软限制指定为0~硬限制范围中的某个值,同时能不可逆转地降低其硬限制。

授权进程(root用户)可以任意改变其软硬限制。

函数原型:

cs 复制代码
#include <sys/time.h>
#include <sys/resource.h>

int getrlimit(int resource, struct rlimit *rlim);
int setrlimit(int resource, const struct rlimit *rlim);

rlimit结构体定义如下:

cs 复制代码
struct rlimit {
  rlim_t rlim_cur; // 软限制
  rlim_t rlim_max; // 硬限制
};

resource的选择有:

cs 复制代码
RLIMIT_AS //进程的最大虚内存空间,字节为单位。
RLIMIT_CORE //内核转存文件的最大长度。
RLIMIT_CPU //最大允许的CPU使用时间,秒为单位。当进程达到软限制,内核将给其发送SIGXCPU信号,这一信号的默认行为是终止进程的执行。然而,可以捕捉信号,处理句柄可将控制返回给主程序。如果进程继续耗费CPU时间,核心会以每秒一次的频率给其发送SIGXCPU信号,直到达到硬限制,那时将给进程发送 SIGKILL信号终止其执行。
RLIMIT_DATA //进程数据段的最大值。
RLIMIT_FSIZE //进程可建立的文件的最大长度。如果进程试图超出这一限制时,核心会给其发送SIGXFSZ信号,默认情况下将终止进程的执行。
RLIMIT_LOCKS //进程可建立的锁和租赁的最大值。
RLIMIT_MEMLOCK //进程可锁定在内存中的最大数据量,字节为单位。
RLIMIT_MSGQUEUE //进程可为POSIX消息队列分配的最大字节数。
RLIMIT_NICE //进程可通过setpriority() 或 nice()调用设置的最大完美值。
RLIMIT_NOFILE //指定比进程可打开的最大文件描述词大一的值,超出此值,将会产生EMFILE错误。
RLIMIT_NPROC //用户可拥有的最大进程数。
RLIMIT_RTPRIO //进程可通过sched_setscheduler 和 sched_setparam设置的最大实时优先级。
RLIMIT_SIGPENDING //用户可拥有的最大挂起信号数。
RLIMIT_STACK //最大的进程堆栈,以字节为单位。

返回值:

成功执行时,返回0。失败返回-1,errno被设为以下的某个值

EFAULT:rlim指针指向的空间不可访问

EINVAL:参数无效

EPERM:增加资源限制值时,权能不允许

相关推荐
承渊政道10 小时前
Linux网络学习【Reactor反应堆模式】
linux·网络·学习·ubuntu·reactor
橙橙笔记11 小时前
具身智能的学习 - 完成一个机械臂 第1节
python·学习·机械臂·具身智能
成都蜀味缘餐饮技术培训学校11 小时前
手撕烤兔技术培训实战指南,秘制腌料配方比例与调制手法
大数据·经验分享·笔记·其他·微信公众平台
希和202712 小时前
免费AI视频生成工具汇总(国内优先,适合做漫剧、短视频,Obsidian笔记版)
人工智能·笔记·音视频
且从容.13 小时前
ubuntu安装flink
linux·ubuntu·flink
蒸蒸yyyyzwd14 小时前
秋招学习笔记 day109
笔记·八股
爱和冰阔落14 小时前
【Linux网络】TCP 服务器如何并发处理多个客户端?从 fork、僵尸进程到双重 fork
linux·运维·服务器·网络
程序员沉梦听雨14 小时前
WSL2的安装与使用
linux·wsl
微小的鱼14 小时前
Linux的渲染与显示系统链路
android·java·linux
Hhy_110715 小时前
《C++深度解构06》手撕 string —— 从底层内存管理到深浅拷贝彻底吃透
c语言·开发语言·c++·学习·visual studio