IMPORTANT

此文档仍需调整补充。

Info

此文档所展示的 Linux 系统库函数定义与实践验证来自于 Ubuntu 22.04。

Linux 系统库是 Linux 操作系统提供,对应用程序提供访问操作系统所管理资源的接口。此篇提供的是基于 C 实现的 Linux 系统库。

系统库中的函数大多数被称为系统调用,通过它们直接向操作系统请求服务或资源。

1 操作系统内核

操作系统内核是与硬件高度相关的以及模块化程序集合体,常驻于内存;

内核的核心功能是 CPU 调度和内存管理;其他功能由模块提供,例如文件系统、各种设备的驱动程序。(特定文件系统有可能直接内置于内核作为默认支持)

2 RAM 存储结构

此内存结构是从逻辑层面单个进程的角度来看的,操作系统会将其抽象为虚拟内存,实际存储可能分散于物理内存的多个区域,并不代表整个内存空间的分布。

从高地址开始,依次为:

  • 内核空间:内核保留的部分,处理该进程的系统调用和中断;实际上的内核空间为进程共享的内存区域;

  • 栈 RAM:自动存储函数调用及其参数、局部变量、返回地址; 栈内存向低地址分配,栈的分配是连续的,内存分配与使用效率高,但栈的可用空间有限,常为 10MB 以下;

  • 内存映射段:进程使用的文件和设备映射,或者较大堆分配请求的映射;运行库文件和共享内存的映射也在此处;

  • 堆 RAM:由程序申请的内存将分配到堆内存空间,也应由程序自行释放; 堆内存向高地址分配,堆的可用空间较大,但是堆是链式分配的,需要链表记录,运行一段时间后申请较大内存可能需要较大查询与移动开销,内存分配与使用效率稍低;

  • 未初始化数据:存储程序未初始化的全局变量与静态变量,Linux 内核会初始化为0;

    WARNING

    即使 Linux 内核会将未初始化数据进行初始化,也需要显式地使用 memset() 完成初始化,避免无效数据。

  • 已初始化数据:存储已由程序初始化的全局变量与静态变量,在程序启动时便可确定大小;

  • 代码与硬编码文本:存储程序经编译汇编后的二进制机器指令,以及硬编码文本,不可修改;此段可与其他程序共享以节省内存。

TIP

有关 RAM 分配的额外信息

  • 栈内存分配运算由 CPU 指令完成;
  • 如果函数递归或嵌套太深,将可发生栈溢出问题;
  • 如果任意1个情况发生:^1^申请的内存起始值丢失;^2^在函数内申请以后未传回更新的内存起址且未将其释放,将可发生内存泄漏问题。

3 异常处理

在调用了系统库函数以后,有可能会产生错误,产生错误时被调用的系统库函数将设置全局变量 errno。要检查并输出错误信息,需要先导入 errno.h(错误码 Macro 定义处),string.h(输出错误信息);

调用系统库函数后,它们一般会返回运行状态并设置错误码全局变量 errno。返回0时表示运行成功,任何非0值一般视为发生错误。应该在系统库函数调用以后根据返回值判断其运行结果,并通过 errno 获取错误类型;

在发生错误时,要获取可阅读的错误信息,应该将返回的非0值作为参数传递给 strerror() 或使用 perror() 直接输出到标准输出;

错误码定义在 errno-base.herrno.h(在 /usr/include/asm-generic/)头文件。

4 缓冲

缓冲可以减少对外部设备的读写频率,提高系统运行效率,常设置于内存。

在 Linux,缓冲区的大小一般为 1024B (glibc);Windows 的缓冲区大小一般为 512B (mingw/MSVC);

可通过以下系统函数修改缓冲区大小:

int setvbuf(FILE *__restrict__ __stream, char *__restrict__ __buf, int __modes, size_t __n);
// Usage example
setvbuf(stdout, NULL, _IOLBF, 4096);

4.1 缓冲类型

  • 全缓冲:仅在缓冲区满或使用 fflush() 时输出,一般用于文件读写;
  • 行缓冲:在缓冲区满、遇到换行符或使用 fflush() 时输出,一般用于终端交互;
  • 无缓冲:直接输出。

4.2 标准输出

与标准输出 stdout (C)/ STDOUT_FILENO (Linux FD) 类似,错误输出 stderr (C)/ STDERR_FILE (Linux FD) 也能将信息输出到控制台或指定设备,但是错误输出不具备缓冲区,发往错误输出设备的将立刻发送到控制台;而标准输出仅在缓冲区刷新和换行符时才会发送到控制台。

缓冲区刷新的常见情况:

  • 使用 fflush(stdout) 刷新标准输出缓冲区;
  • 缓冲区满或有换行符;
  • **进程(?)**运行结束。

4.3 异常处理函数

将错误码转换为字符串

需要导入 string.h

char* strerror(int __errnum)

参数

错误码值,值应当为非负数。

返回值

此错误码对应的错误消息。

将自定义信息连带错误信息输出到标准错误输出

需要导入 stdio.h

void perror(const char *__s)

参数

作为消息发送到标准输出设备的一段自定义字符串。

输出形式为 __s : strerror(errno)

5 文件系统

5.1 文件描述符

用于标识已打开的文件。

在 Linux 系统中,文件描述符 (File descriptor, FD) 是用来标识一个特定进程已载入的输入输出设备已开启的套接字和文件系统中已打开的文件的非负数整型值;

Linux 操作系统会为启动的进程默认分配3个基本设备的文件描述符:

  • 标准输入(键盘),在 C 库位于 stdin->_fileno,在 Linux 系统库为 STDIN_FILENO,文件描述符为0;
  • 标准输出(控制台),在 C 库位于 stdout->_fileno,在 Linux 系统库为 STDOUT_FILENO,文件描述符为1;
  • 错误输出(控制台),在 C 库位于 stderr->_fileno,在 Linux 系统库为 STDERR_FILENO,文件描述符为2。

文件描述符具备以下特性:

  • 进程独享:文件描述符仅在当前进程及其子进程下有效;
  • 动态分配:使用 open() 建立,总是从最小可用文件符ID分配,在使用 close() 或进程结束时清除;
  • 可用于重定向:能够通过文件标识符将输出重定向到目标文件;
  • 通用性:文件标识符不仅可以对应文件,还可对应作为文件处理的设备和网络套接字。

TIP

文件指针 FILE* 和 文件描述符 int 的相互转换

  • 文件描述符 → 文件指针

    fdopen(int file_descriptor, open_mode)

  • 文件指针 → 文件描述符

    fileno(FILE* file_pointer)

复制文件、设备或套接字接口

将文件、设备或套接字接口从 __fd 复制到 __fd2,这会使 __fd2 对应的文件、设备或套接字对此进程关闭,并使 __fd__fd2 对应同一个文件、设备或套接字。

int dup2(int __fd, int __fd2)

参数

  1. [对应文件必须打开] 待复制的文件描述符;
  2. [对应文件将会关闭] 待关闭并接受复制的文件描述符。

返回值

如果运行成功,返回新的 __fd2 值;其他情况返回-1并设置 errno 值;这可以视为文件描述符 __fd2 成为了 __fd 对应文件、设备或套接字的别名,因此可以用于IO重定向。

TIP

要停用来自于标准IO设备的可见输入和输出,可打开 /dev/null 黑洞设备,并使用 dup2 以将标准IO设备的输入输出重定向到此黑洞设备。

int null_dev_fd = open("/dev/null", O_RDWR);
dup2(null_dev_fd, STDIN_FILENO);
dup2(null_dev_fd, STDOUT_FILENO);
dup2(null_dev_fd, STDERR_FILENO);

5.2 文件操作函数

需要导入 unistd.hfcntl.h

获取当前运行路径

char* getcwd(char *__buf, size_t __size)

修改运行路径

int chdir(const char *__path)

参数

目标运行路径;

返回值

若运行成功,返回0;其他情况返回-1并设置 errno 值。

获取文件权限

int access(const char *__name, int __type)

参数

  1. 文件路径;

  2. 权限类型,可以使用以下任意类型的组合:

    F_OK 文件存在;R_OK 文件可读;W_OK 文件可写;X_OK 文件可运行。

返回值

若文件存在且指定权限均可用,返回0;其他情况返回-1并设置 errno 值。

设置新建文件的权限遮罩值

mode_t umask(mode_t __mask)

参数

新的权限遮罩值,应为3位八进制正整型,例如 0022

返回值

原权限遮罩值。

注意,新遮罩值仅在当前进程中有效。

Info

Q. 如何计算新建文件的权限?

  • 先确定遮罩值,将遮罩值转为权限字串;例如 0154 → --xr-xr--
  • 若创建文件,将 rw-rw-rw- 减去 --xr-xr--; 若创建目录,将 rwxrwxrwx 减去 --xr-xr--
  • 得到创建文件时的权限值:rw--w--w-; 或者创建目录时的权限值:rw--w--wx

不要直接将基准权限数和遮罩值相减!这会得出错误结果。

打开或创建一个文件

int open(const char *__path, int __oflag, ...)

参数

  1. 该文件的路径;

  2. 文件打开方式与选项,支持使用或运算符选择多个选项。

    • 必须指定一种打开方式: O_RDONLY 只读;O_WRONLY 只写;O_RDWR 可读可写。

    • 可用选项:

      O_CREAT 文件不存在时,创建; [需要与 O_CREAT 共用] O_EXCL 文件存在时,失败; [可写时] O_TRUNC 文件存在时,清空; [可写时] O_APPEND 写入到文件尾; O_NONBLOCK 非阻塞模式,用于设备文件和管道(?)[可写时] O_SYNC 同步写,写入时立刻写外部存储; O_CLOEXEC 运行 exec 时关闭文件描述符。

  3. [可选,仅创建文件时] 设置新文件的权限,会受到 umask 值的影响。

Info

创建文件时的实际赋予权限是:设置的权限值 & ~umask 值

可使用以下函数直接创建文件,0*** 表示为3位八进制数:

int creat(const char *__file, 0***);

这个函数与下面这段代码效果相同:

open(char *file_path, O_WRONLY | O_CREAT | O_TRUNC, 0***);

如果有可能创建文件时,应该设置文件权限;其默认权限较低,若需再次打开时可出现权限不足打开失败的问题。

返回值

如果成功打开文件,将返回文件标识符ID;如果失败,将返回-1并设置 errno 值。

关闭打开的文件

int close(int __fd)

参数

文件标识符。

返回值

若正确关闭,返回0;其他情况返回-1并设置 errno 值。

读一个文件

ssize_t read(int __fd, void *__buf, size_t __nbytes)

参数

  1. 待读取文件标识符;
  2. 写入目标的起始指针;
  3. 读取的字节数量。

返回值

成功读取的字节数;有错误时返回-1并设置 errno 值,读到文件尾时返回0.

WARNING

不能使用 EOF Macro 作为读到文件尾的判断条件,EOF 在C标准库中定义为-1。

写一个文件

ssize_t write(int __fd, const void *__buf, size_t __n)

参数

  1. 待写入文件标识符;
  2. 读取目标的起始指针;
  3. 写入的字节数量。

返回值

成功写入的字节数;有错误时返回-1并设置 errno 值。

移动文件光标

__off_t lseek(int __fd, off_t __offset, int __whence)

参数

  1. 已打开文件的标识符;

  2. 整型偏移量,支持正值和负值;

  3. 基准位置,应该为以下值: SEEK_SET 文件头,偏移量只能为正值;

    SEEK_CUR 当前位置;

    SEEK_END 文件尾,偏移量只能为负值。

返回值

运行成功时,返回此文件光标位置字节数;有错误时返回-1并设置 errno 值。

删除一个文件

此函数不能删除目录。

int unlink(const char *__name)

参数

待删除文件的路径。

返回值

删除成功时,返回0;其他情况返回-1并设置 errno 值。

5.3 目录操作函数

需要导入 dirent.h

目录是特殊的文件,用于对文件进行分类以便于用户管理。

创建一个目录

int mkdir(const char *__path, mode_t __mode)

参数

  1. 待创建目录的路径;

  2. 创建目录时设置的权限,可以是以下每组选项中的1个:

    • S_IRWXU 所有者可读可写可运行;

      S_IRUSR 所有者可读;

      S_IWUSR 所有者可写;

      S_IXUSR 所有者可运行;

    • S_IRWXG 所有者用户组可读可写可运行;

      S_IRGRP 所有者用户组可读;

      S_IWGRP 所有者用户组可写;

      S_IXGRP 所有者用户组可运行;

    • S_IRWXO 其他用户可读可写可运行;

      S_IROTH 其他用户可读;

      S_IWOTH 其他用户可写;

      S_IXOTH 其他用户可运行。

返回值

如果创建成功,返回0;其他情况返回-1并设置 errno 值。

打开一个目录

DIR* opendir(const char *__name)

参数

目录所在路径。

返回值

如果打开成功,返回目录流指针;其他情况返回 NULL 并设置 errno 值。

关闭打开的目录

int closedir(DIR *__dirp)

参数

目录流指针。

返回值

如果正确关闭,返回0;其他情况返回-1并设置 errno 值。

读取下一个目录项

struct dirent* readdir(DIR *__dirp)

参数

目录流指针。

返回值

目录项目结构体 dirent;其部分成员有:

  • 文件名 d_name
  • 文件类型 d_type
  • 文件索引结点号 d_ino

需要手动使用循环完成遍历。当到达目录末尾或其他情况时返回 NULL,如果出现错误,还会设置 errno 值;读取的目录项依次为该目录的所有文件。

删除一个空目录

int rmdir(const char *__path)

参数

待删除空目录的路径。

返回值

删除成功时,返回0;其他情况返回-1并设置 errno 值。

删除一个文件或空目录

int remove(const char *__filename)

参数

待删除文件或空目录的路径。

返回值

删除成功时,返回0;其他情况返回-1并设置 errno 值。

5.4 文件锁

文件锁是 Linux 系统提供的,为用户对多进程协调文件资源使用关系的一种机制;使用文件锁可使多进程同时使用同一个文件时协调有序,避免出现数据错误。

文件锁是非强制型的,如果程序不检测文件锁,它仍会正常操作此文件;

文件锁锁上时,若进程关闭文件描述符,文件锁自动解锁;

子进程不继承文件锁

文件控制函数

int fcntl(int __fd, int __cmd, ...)

参数

  1. 文件描述符;

  2. 操作指令 Macro,可以是以下任意一个: F_DUPFD 复制文件描述符;

    F_GETFD 获取文件描述符;

    F_SETFD 设置文件描述符;

    F_GETFL 获取文件状态;

    F_SETFL 设置文件状态;

    F_GETLK 查询是否可创建读或写锁;

    F_SETLK 设置文件锁。

  3. 可变参数 如果是传入设置类型的操作指令和 F_GETLK,需要填入此参数。

    F_SETFD 可用选项:

    • FD_CLOEXEC 运行exec类函数时,关闭此文件描述符;

    F_SETFL 可用选项:

    • O_NONBLOCK 设置文件IO为非等待型;
    • O_APPEND 追加写入;

    F_GETLK 需要的参数类型:struct flock*(锁类型不能是 F_UNLCK),结构体见下方 NOTE 注记;

    F_SETLK 需要的参数类型:struct flock*

Info

若第2个参数为 F_SETFL,需要先获取 F_GETFL,然后将获取到的值与 F_SETFL 可用选项进行或运算,再将此值作为第3个参数。

int flag = fcntl(STDIN_FILENO, F_GETFL);
int status = fcntl(STDIN_FILENO, F_SETFL, flag | O_NONBLOCK);

flock 结构体

struct flock {
 short int l_type;   /* Type of lock: F_RDLCK, F_WRLCK, or F_UNLCK. */
 short int l_whence; /* Where `l_start' is relative to (like `lseek'). */
 __off_t l_start; /* Offset where the lock begins. */
 __off_t l_len;   /* Size of the locked area; zero means until EOF. */
 __pid_t l_pid;   /* Process holding the lock. */
};

注意,设置的 flock 结构体中的锁类型要求打开的目标文件含有此种打开方式:

int fd = open("test.txt", O_RDONLY);
//                        ^^^^^^^^
struct flock file_lock = {
 .l_type = F_RDLCK,       // Correct ✅
 // .l_type = F_WRLCK,    // Incorrect ❌
 .l_whence,
 .l_start,
 .l_len,
 .l_pid
};
 
fcntl(fd, F_SETLK, &file_lock);

返回值

  • F_DUPFDF_GETFD 运行成功时,指令返回文件描述符;
  • F_GETFL 运行成功时,指令返回状态位;
  • F_GETLK
    1. 参数正确时,返回0,并将上锁信息写入到传入的结构体指针;如果 l_type = F_UNLCK,表示可以设置读写锁;如果 l_type = F_RDLCK,可设置读锁;如果 l_type = F_WRLCK,不能设置锁。
    2. 参数错误时,返回-1并设置 errno 值;
  • F_SETLK
    1. 上锁成功时,返回0;
    2. 上锁失败时,返回-1,并设置 errno(Resource Temporarily Unavailable)

6 进程与线程

IMPORTANT

在现代通用操作系统,进程仅作为资源分配单位,线程成为调度 CPU 运行的单位;创建进程后实际上至少建立一个主线程以使程序运行。

6.1 进程 Process

操作系统将可运行文件加载到内存,并为其分配内存空间和资源,这就构成了一个进程

进程又可以定义为:程序在一个数据集合(代码+数据)上运行的过程;

程序则是存储在外部存储的数据集合。

进程的特性:

  • 动态性:进程在操作系统运行期间创建或结束;
  • 并发性:多个进程总是在一段较短的时间内共同运行;
  • 独立性:进程是操作系统资源分配的基本单位;
  • 异步性:因为并发性,不能准确判断进程何时结束。

特别的进程

有些进程因为其特质和特定条件产生而被赋予了特别的名字。

  • 孤儿进程:父进程建立子进程,父进程未进行等待且首先结束。此时子进程就是孤儿进程;孤儿进程会被系统 init / systemd 进程 (PID = 1) 接管,被接管后子进程运行结束时会被 init / systemd 进程清理;
  • 僵尸进程:父进程建立子进程,在子进程运行结束后父进程未通过 wait()waitpid() 获取子进程状态,直到父进程结束为止;它的内存和资源已被操作系统回收,但其进程控制块仍然存在;
  • 守护进程 (Daemon):常驻于后台的,不通过终端交互的,周期进行任务处理(进程监视、活动记录等)的进程。

TIP

创建守护进程的主要过程

  1. 使用 fork() 建立子进程并获取返回值;

  2. 判断进程创建状态,如果创建成功,通过 if(pid > 0) 使主进程退出而子进程继续运行;

  3. 为使子进程脱离终端且独立运行,使用 setsid() 创建会话组,会话组ID的值为子进程PID;

  4. 需要时可通过 umask() 修改父进程的文件创建遮罩值;

  5. 为避免文件系统无法卸载,需要通过 chdir() 修改子进程工作路径到根目录 /

  6. 守护进程在后台静默运行,所以需要停用与标准IO设备的交互,通过以下代码完成:

    int null_device_fd = open("/dev/null", O_RDWR);
    dup2(null_device_fd, STDIN_FILENO);
    dup2(null_device_fd, STDOUT_FILENO);
    dup2(null_device_fd, STDERR_FILENO);
  7. 设置守护进程的主循环,然后设置所需任务。

手动控制进程运行

Ctrl+Z 暂停前台进程,置入后台作业列表;

jobs -l 查看后台与挂起的作业;

fg %job_id 将进程恢复到前台运行;

bg %job_id 将进程恢复到后台运行;

kill -9 %job_id 将进程终止。

6.2 线程 Thread

将进程当中的任务分为多个小任务,这个小任务一般以函数形式存在,每个小任务就是一个线程,这也被称为轻量级进程。线程是操作系统进行 CPU 调度的单位。进程内,线程间共享同一个进程的数据内存和资源,应注意资源访问控制。

程序未创建线程时,就是单线程程序;main() 被称为主线程;在 main() 创建的线程被称为子线程;线程的创建也可以在子线程;主线程运行结束等同于进程结束,所有其他线程都将结束。

在 Linux 系统,线程主要依靠函数封装来进行运作。

6.3 会话 Session

会话的通常含义是用户登入操作系统后,操作系统提供的一个交互接口,以及为实现用户交互所启动的一系列进程。概括地说,会话就是一组进程的集合体

用户的登入就可以视为启动了一个新的会话。

6.4 进程操作函数

查询本进程的进程号

pid_t getpid(void)

返回值

当前进程的 PID。

创建一个子进程

pid_t fork(void)

返回值

如果创建成功,当前程序代码、环境变量将共享给子进程,数据部分将在有写入时复制 (Copy on Write) 给子进程;父进程中,fork() 的返回值为子进程的 PID,子进程中,fork() 的返回值为0;子进程将从 fork() 开始运行;可通过此机制使用 if 语句区分进程运行的代码;

如果创建失败,将返回-1并设置 errno 值。

Info

旧版本创建子进程的函数

pid_t vfork(void)

此函数创建的子进程的主要特点是:

  • 父子进程共享内存空间,因此创建子进程也无需复制页表;
  • 子进程将先运行,并使父进程等待,直到子进程调用 exec()_exit()

所以,使用子进程应该注意:

  • 不要修改父进程的数据;
  • 使用 _exit() 退出,避免刷新标准缓冲区;
  • 不应该在当前运行起点函数内使用 return,避免栈异常。

向本进程发送信号

需要导入 signal.h 以使用信号 Macro。

int raise(int __sig)

参数

信号ID。

返回值

如果运行成功,程序无法获得返回值;其他情况返回-1并设置 errno 值。

向进程发送信号

需要导入 signal.h 以使用信号 Macro。

int kill(pid_t pid, int sig)

参数

  1. 要发送的目标进程,除了指定进程标识符,还支持以下特定用法:
    • pid = 0 发送给当前进程组内的全部进程;
    • pid = -1 发送给当前进程可发送的所有进程,不含 init / systemd 进程;
    • pid < -1 发送给进程组为 |pid| (绝对值)的全部进程。
  2. 要发送的信号。

返回值

如果运行成功,返回0;其他情况返回-1并设置 errno 值。

设置本程序的信号处理函数

需要导入 signal.h 以使用信号 Macro。

使程序捕获指定的信号,并加以处理或选择忽略此信号。

__sighandler_t signal(int __sig, __sighandler_t __handler)

参数

  1. 要捕捉处理的信号ID,终止信号必须是可以捕获的;
  2. 信号的处理函数,处理函数可设置 int 参数以获取信号ID;若需要忽略信号,填入 SIG_IGN

返回值

成功时返回0,其他情况返回-1并设置 errno 值。

Info

  • 向进程发送信号时,若目标进程未忽略此信号,sleep() 将提前返回;
  • 进程未忽略且未捕获的信号将由操作系统处理;并运行该信号的默认处理行为;以下有 OS 处理时终端返回码 值的表示该进程的默认行为是立刻结束或暂停;
  • 如果程序未运行到 exit()main() 的末尾就被结束,退出状态码将由操作系统返回。

✅ 以下信号可捕获或忽略:

信号IDOS 处理时
终端返回码
Macro 定义含义
1129SIGHUP终端挂断
2130SIGINT请求前台进程终止 (Ctrl+C)
3131SIGQUIT请求前台进程终止 (Ctrl+\),生成核心转储文件
10/SIGUSER1用户自定1
12/SIGUSER2用户自定2
13141SIGPIPE管道破裂(例如尝试向 0.0.0.0 建立端口连接)
14142SIGALRM定时器时间到
15143SIGTERM请求进程终止(指令 kill 的默认行为)
17/SIGCHLD子进程状态改变
18/SIGCONT使暂停进程继续运行
20[非终止] 148SIGSTP请求暂停进程
21/SIGTTIN后台进程尝试读取终端
22/SIGTTOU后台进程尝试写入终端
23/SIGURG紧急数据到达
28/SIGWINCH终端窗口尺寸改变
29/SIGIO文件描述符就绪

⛔ 以下终止信号可被捕获或忽略,但应谨慎处理:

信号IDOS 处理时
终端返回码
Macro 定义含义
4132SIGILL非法指令,生成核心转储文件
6134SIGABRT使进程中止(断言失败),生成核心转储文件
7135SIGBUS总线错误(内存对齐异常),生成核心转储文件
8136SIGFPE浮点异常(除以0),生成核心转储文件
11139SIGSEGV段错误(内存访问错误),生成核心转储文件

❌ 以下信号不能被捕获或忽略:

信号IDOS 处理时
终端返回码
Macro 定义含义
9137SIGKILL强制进程终止
19[非终止] 147SIGSTOP强制进程暂停

[高级] 设置本程序的信号处理函数

需要导入 signal.h 以使用信号 Macro。

此信号处理函数可提供额外的信号屏蔽、自动重启系统调用、获取信号来源及更佳的多线程安全性。

int sigaction(int sig, const struct sigaction *act, struct sigaction *oldact)

参数

  1. 要捕获的目标信号;
  2. 捕获信号后的操作,结构体释义见下方 NOTE 注记;
  3. 可存储以前捕获信号时的操作,如果不需要可设置 NULL。

返回值

Info

sigaction 结构体

struct sigaction {
    void     (*sa_handler)(int);      // 简单处理函数(类似 signal() 第2参数)
    void     (*sa_sigaction)(int, siginfo_t *, void *); // 高级处理函数
    sigset_t sa_mask;                // 执行处理函数时阻塞的信号集
    int      sa_flags;               // 控制标志(如 SA_RESTART)
};

处理函数入口的设定在 (*sa_handler)() 函数指针和 (*sa_sigaction)() 函数指针,根据需要任选其一;默认情况下使用 (*sa_handler)() 提供的函数;要使用 (*sa_sigaction)(),需要为 sa_flags 设置 SA_SIGINFO

以下是使用 sigaction() 的示例。

示例1. 运行以下4条语句后,进程在收到 SIGTERM 后将转入 advanced_signal_handler()

struct sigaction sig_act;
sig_act.sa_handler = advanced_signal_handler;
sigemptyset(&sig_act.sa_mask);
sigaction(SIGTERM, &sig_act, NULL);

示例2. 运行以下4条语句后,进程将忽略 SIGQUIT 信号:

signal(SIGINT, SIG_IGN);
struct sigaction sa;
sa.sa_handler = SIG_IGN;
sigaction(SIGQUIT, &sa, NULL);

子进程返回等待同步

需要导入 sys/wait.h

该函数被父进程调用后,父进程将等待到子进程运行完成后再继续运行,并获取子进程返回的状态码。

pid_t wait(int *__stat_loc)

参数

将存储子进程退出状态信息的整型指针,这在子进程中可使用 exit(__stat_loc)main()return 指定此值。

返回值

运行成功时,返回结束的子进程PID;

其他情况时,返回-1并设置 errno 值。

Info

假设通过整型状态信息 status 获取 wait() 的返回值,可用下面的 Macro 进行检查:

  • [判断] WIFEXITED(status):如果子进程通过 exit()main()return 结束,返回1;
    • WEXITSTATUS(status) [仅当 WIFEXITED 为真时有效] 返回子进程的退出状态值;
  • [判断] WIFSIGNALED(status):如果子进程因信号而终止,返回1;
    • WTERMSIG(status) [仅 WIFSIGNALED 为真时有效] 返回导致子进程终止的信号ID;要转换为字符串,可使用 strsignal()
  • [判断] WIFSTOPPED(status):如果子进程被停止,返回1;
    • WSTOPSIG(status):返回导致子进程停止的信号编号。

替换当前进程运行的程序下文(exec 系列函数)

需要导入 unistd.h

如果通过此函数指定的目标程序存在且可访问,当前进程的后续程序代码将被取代为指定的程序。

通过 exec 系列函数运行系统指令时,一般的传递顺序是:指令名称、其他选项;最后一个参数必须设置为 NULL。

1 (以形参列表传递参数时)

1.1 (一般形式)

int execl(const char *__path, const char *__arg, ...)

1.2 (变体:自动检索系统环境变量)

int execlp(const char *__file, const char *__arg, ...)

1.3 (变体:可指定环境变量)

int execle(const char *__path, const char *__arg, ...)

参数

  1. 目标程序的路径;
  2. (第 2~n 个参数) 要传递给目标程序的形式参数顺序;
  3. [仅函数1.3] (第 n+1 个参数) 指定的环境变量,以字符指针数组形式传递,且此数组也要以 NULL 结尾。

返回值

如果成功开始运行目标程序,返回值不起作用,因为程序已被替换;

如果未能运行目标程序,将返回-1并设置 errno 值。

2 (以字符指针数组传递参数时)

2.1 (一般形式)

int execv(const char *__path, char *const __argv[])

2.2 (变体:自动检索系统环境变量)

int execvp(const char *__file, char *const *__argv)

2.3 (变体:可指定环境变量)

int execve(const char *__path, char *const *__argv, char *const *__envp)

参数

  1. 目标程序的路径;
  2. 以字符指针数组形式传递给目标程序的参数列表;
  3. [仅函数2.3] 指定的环境变量,以字符指针数组形式传递,且此数组也要以 NULL 结尾。

返回值

如果成功开始运行目标程序,返回值不起作用,因为程序已被替换;

如果未能运行目标程序,将返回-1并设置 errno 值。

创建新的会话

pid_t setsid(void)

返回值

返回会话ID,会话ID在值上就是会话建立进程PID值。

6.5 线程操作函数 (POSIX)

现代 Linux 内核遵循 POSIX 标准线程机制实现,支持内核级线程。

TIP

遵循 POSIX 标准的线程函数,可通过 strerror() 获取错误的可读信息;

可通过 __func__ 获取所在函数的标识符。

Info

运行本节提到的函数,需要导入 pthread.h,并且可能需要在编译指令中加入 -lpthread 选项。

查询本线程的线程号

pthread_t pthread_self(void)

返回值

此线程的ID。

创建新的线程

将一个函数作为线程运行,可以多次创建一个函数的线程。

int pthread_create(pthread_t *__restrict__ __newthread, const pthread_attr_t *__restrict__ __attr, void *(*__start_routine)(void *), void *__restrict__ __arg)

参数

  1. 传回新线程 ID 的指针 long unsigned int
  2. 要设定的线程属性,无需设定时可传入 NULL;
  3. 代表此线程的函数标识符,不需要添加括号;
  4. 要传给线程的参数。

返回值

创建成功时,返回0;其他情况时返回错误码。

线程返回等待同步

调用此函数的线程需等待目标线程运行完成后才能继续运行。

int pthread_join(pthread_t __th, void **__thread_return)

参数

  1. 等待的目标线程 ID;
  2. 存储目标线程返回值地址的指针。

返回值

在目标线程运行结束后,返回0;其他情况返回错误码。

Info

线程函数的返回类型建议设为 void* 任意类型指针,在 pthread_join() 函数处用变量接收返回值时仅需进行强制转换即可,或者使用 void* 类型变量接收。

请求取消线程运行

此函数运行成功后,将停止目标线程的运行,但还需要使用 pthread_join() 以清理其占用的资源。

int pthread_cancel(pthread_t __th)

参数

要取消的目标线程 ID。

返回值

如果成功取消线程,返回0;其他情况返回错误码。

Info

能不能取消目标线程,以及取消时线程到达的代码位置,由目标线程的属性设置决定:

  • 通过 pthread_setcancelstate() 可设置:
    • [默认] PTHREAD_CANCEL_ENABLE 接受到达的取消请求;
    • PTHREAD_CANCEL_DISABLE 忽略到达的取消请求。
  • 通过 pthread_setcanceltype() 可设置:
    • [默认] PTHREAD_CANCEL_DEFERRED 在取消检查点检查取消请求;
    • [不安全] PTHREAD_CANCEL_ASYNCHRONOUS 在任何代码位置检查取消请求。

取消检查点为:多数等待型函数调用处;显式调用 pthread_cancel();标准IO设备的输入输出。

将线程分离

此函数运行成功后,在该线程运行结束时,操作系统将立刻释放此线程的栈、线程描述符和其他资源;且无法获取此线程的返回值。

int pthread_detach(pthread_t __th)

参数

要分离的目标线程 ID。

返回值

如果运行成功,返回0;其他情况返回错误码。

终止线程

此函数仅用于线程本身发起终止,不能终止其他线程。

void pthread_exit(void *__retval)

参数

若需要进程的返回值时,传入接受返回值的指针,可以为 NULL。

6.6 同步&互斥

在支持多任务的系统中,进程与线程以并发或并行(仅多核处理器)态运行。但因为实现并发的调度算法需要依据实时情况调度,常常使进程与线程运行顺序不易预测,且每次运行耗时也变得更难确定,这就是并发或并行带来的异步性

更重要的是,多任务系统还需要考虑独占型或写数据访问资源(这称为临界资源)的访问顺序,资源就是供进程完成既定任务的必要成分,它们可以是硬件资源,也可以是软件资源。 硬件资源是各种设备。其中以外部设备为典型,很多外部设备为串行通信设备,它们一般无法同时被许多进程写访问,这必然造成数据错乱; 软件资源是内存或硬盘中的数据。若不对其中的共享数据部分进行访问协调,也必然会使每次进程运行结果不确定,甚至是进行不确定的行为导致系统崩溃。

为了协调对临界资源的访问,保障数据安全与运行过程与结果可靠,需要引入同步与互斥访问功能。

同步与互斥是进程之间或线程之间协调共享数据的访问次序,保证数据一致性,避免产生数据异常的一种概念统称。

同步(直接制约):一个进程或线程等待其他进程或线程完成既定的任务,然后继续运行;

互斥(间接制约):多个进程或线程同时写资源时,使同一时刻仅一个进程或线程可写。

临界资源:只能独占访问的资源(大多数输出型设备,例如打印机;大多数写操作);

临界区:自请求资源开始至释放资源,访问临界资源的代码区间。

同步的规则:

  • 空闲让进:若共享资源可用,需要使线程可访问;
  • 忙则等待:若共享资源全部正在使用,需要使请求线程等待;
  • 有限等待:请求资源的线程不能长时间等待,必须能够在可控时间内最终获得资源或运行其他任务;
  • 让权等待:使等待资源的线程立刻失去 CPU,不应使 CPU 频繁检测进入临界区的条件。

死锁

若有多个进程同时请求临界资源,且它们已持有部分其他线程正请求的临界资源时,这些进程就会陷入无尽等待,这被称为死锁。

另一个更加正式的解释是:一组等待进程中,每个进程都在等待仅组内进程能产生的事件,并构成等待环。

产生死锁的条件是:

  • 资源是互斥访问的;
  • 进程持续拥有资源并请求新资源;
  • 进程已获取的资源不能被转移;
  • 若绘制等待图,构成环。

同步与互斥操作

以下对共享资源访问的工具由操作系统支持,是线程级别的。使用它们需要导入 pthread.h

要在进程之间实现同步与互斥,需要结合共享内存,或者使用 SystemV 提供的信号量(集)。

互斥锁 pthread_mutex 及其使用

互斥锁使用了基于操作系统的同步原语,仅1个线程能得到互斥锁,锁定时其他线程的锁请求会使其等待;释放锁后再分配给正等待锁的其他线程。在代码内要确保锁定后也能够释放。

Info

线程互斥锁仅适用于数量为1个的,或要进行写数据控制的资源;

使用线程互斥锁后,线程对共享资源的访问顺序仍不确定。

  1. 建立线程互斥锁变量;

    pthread_mutex_t access_lock = PTHREAD_MUTEX_INITIALIZER;
  2. 在对共享数据进行访问的代码前锁定,得到锁的线程将运行后续代码,未获得锁的线程将转为等待态; 例如写后读取,为保证写入数据与读取数据一致,需要上锁。

    pthread_mutex_lock(&access_lock);
  3. 在共享数据访问的代码之后解锁。

    pthread_mutex_unlock(&access_lock);
条件变量 pthread_cond 及其使用

线程条件变量提供等待与唤醒功能,使线程同步互斥机制能够依赖其他普通变量以决定线程的运行顺序与时机,需要配合线程互斥锁和变量判断使用

  1. 建立线程条件变量和运行次序,建立标志值;

    pthread_cond_t access_next = PTHREAD_COND_INITIALIZER;
    int running_order = 0;
  2. 设置 while 条件,当不是自己的运行标志值时循环等待,此时会释放互斥锁

    pthread_mutex_lock(&access_lock);
    while (running_order != 1) {
        pthread_cond_wait(&access_next, &access_lock);
    }

    WARNING

    调用 pthread_cond_wait() 后,线程将会等待唤醒,在此之后需要其他线程以相同的条件变量使用 pthread_cond_signal() 唤醒,且该唤醒是立刻(不可存储)的,如果无线程正在等待,唤醒操作冗余;

    while 条件的设置是必要的,如果运行标志值是自己的,就应该往下运行而不能调用 pthread_cond_wait()。若运行到最后其他线程都退出,将没有线程能够发送唤醒信号,这会使线程陷入等待。

  3. 脱离循环等待条件唤醒信号后,在共享数据访问的代码之后,发送唤醒信号;这会唤醒正在等待此唤醒信号的线程;

    pthread_cond_signal(&access_next);
  4. 释放互斥锁。

    pthread_mutex_unlock(&access_lock);
信号量 semaphore 及其使用

需要导入 semaphore.h

信号量是互斥锁的强化版本,它基于计数工作,适用于线程对多个同类型互斥资源的访问互斥。

  1. 初始化一个信号量;

    int sem_init(sem_t *sem, int pshared, unsigned int value);

    参数

    1. 信号量指针;
    2. 共享标志,传入0表示线程共享信号量;反之表示进程共享;
    3. 信号量的初始值。

    返回值

    信号量获取成功时,返回0;其他情况返回-1并设置 errno 值。。

  2. PV操作;

    • P 操作

      int sem_wait(sem_t *sem);

      参数

      信号量指针。

      返回值

      若信号量此时>0,-1并返回;

      若信号量此时是0,则等待信号量增加;

      运行成功时,返回0;运行失败时返回-1并设置 errno 值。。

    • V 操作

      int sem_post(sem_t *sem);

      参数

      信号量指针。

      返回值

      信号量值+1;

      如果有线程或进程等待,唤醒一个;

      运行成功时,返回0;运行失败时返回-1并设置 errno 值。。

    • 测试信号量(非等待型 P 操作)

      int sem_trywait(sem_t *sem);

      参数

      信号量指针。

      返回值

      信号量值>0时,返回0;信号量为0时返回-1并设置 errno 值为 EAGAIN;其他情况返回-1并设置 errno 值。

    • 限时 P 操作

      int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);

      参数

      1. 信号量指针;
      2. 时间设定结构体。

      返回值

      得到信号量时,返回0;超时返回-1并设置 errno 值为 ETIMEOUT;其他情况返回-1并设置 errno 值。

  3. 使用后删除信号量;

    int sem_destroy(sem_t *sem);

    参数

    信号量指针。

    返回值

    运行成功时,返回0;运行失败时返回-1;其他情况返回-1并设置 errno 值。

文件锁及其使用

需要导入 fcntl.h。文件锁适用于进程间的互斥。

// TODO

屏障及其使用

屏障是用于线程同步的机制,使线程能够在指定处等待,待所有线程均运行至指定位置时即可解除屏障,使所有线程继续运行。适用于多个线程输出的结果循环输出到一个线程作为其输入。

// TODO

6.7 进程间的通信 IPC

Inter Process Communication

管道

管道是用于进程之间通信的方法之一,两个进程将一个文件视为缓冲区进行通信,本质上是提供了简单封装的文件系统通信,管道一般分为两种类型:

  • 匿名管道 (Pipe):用于衍生自同一进程的进程之间通信;在同一时刻,匿名管道只能单向通信,是半双工的;操作系统设置的匿名管道内存缓冲区常为 64KB,此值可修改。匿名管道在文件系统不可见。
  • 命名管道 (FIFO):用于任何进程之间的通信,命名管道只能是单工。

Info

管道通信一般为单向的,这也是受管道为单个文件所限;要切换传送方向,需要重新打开该管道文件。

匿名管道通信的流程
  1. 在一个进程中声明一个大小为2的整型数组 int[2],用于操作管道;

    int fd[2];
  2. 启用管道。如果成功时返回0,其他情况返回-1并设置 errno 值; 调用 pipe() 后,fd 将拥有管道的读端 fd[0] 和写端 fd[1] 地址。

    pipe(fd);
  3. 现在可创建新进程,假设一端要写入数据,进行以下操作; 写入已满的管道,该进程会等待;如果不需要读取,可直接关闭读端。

    // Optional
    close(fd[0]);
     
    write(fd[1], buffer, sizeof(buffer));
  4. 另一端要读取数据,进行以下操作; 读取空的管道,该进程会等待。如果不需要写入,可直接关闭写端。

    // Optional
    close(fd[1]);
     
    read(fd[0], buffer, sizeof(buffer));
  5. 使用以后,应该像普通文件一样关闭管道。

    close(fd[0]);
    close(fd[1]);
命名管道通信的流程
  1. 建立一个管道文件,建立后的管道文件将可见于文件系统; 通过 Shell 指令建立:

    mkfifo <file_path>

    也可通过 Linux 系统 C 库建立,需要导入 sys/stat.h

    int mkfifo(const char *__path, mode_t __mode);

    __mode 是权限设置参数,与前述权限设置方法一致。

    WARNING

    管道文件名不能已存在,若存在将创建失败。

  2. 像打开普通文件那样打开一个管道文件,但只能以只读 O_RDONLY 或 只写 O_WRONLY 方式打开; 对于写进程:

    int fd = open("path_to_fifo", O_WRONLY);

    对于读进程:

    int fd = open("path_to_fifo", O_RDONLY);
  3. 随后与操作普通文件的流程一致。

信号量(集)(System V)

使用此节提到的函数,需要导入 sys/sem.h。本节使用 System V 库。

信号量 Semaphore 是由操作系统实现的,为实现进程同步而提供的工具;

信号量能够用于事务处理的接力(上一个进程处理数据后,交给下一个正等待数据的进程并唤醒它);模拟互斥锁;

创建信号量集或获取信号量集标识符
int semget(key_t __key, int __nsems, int __semflg)

参数

  1. 信号量集唯一标签键值,用于识别唯一的信号量集,常用 ftok() 生成,本质上是 int
  2. 信号量的种类数,代表不同种资源的数量,如果是建立新的信号量集,必须大于0。创建后无法修改;
  3. 选项与权限标志,选项与权限以或运算进行组合,可用选项为:
    • IPC_CREAT 若指定的信号量集不存在,就新建;否则返回现存的信号量集标识符;
      • IPC_EXCL 若指定的信号量集存在,创建失败;

返回值

信号量集标识符,其他情况返回-1并设置 errno 值。

Info

ftok() 的解释

此函数是 System V 方法提供的用于生成唯一标签的函数,最终用于创建系统级唯一的信号量(集)、共享内存和消息队列。

实现概述

函数将文件路径和一个整数组合并生成一个 id。若期望一些进程能访问共享资源,指定的文件路径和整数应当相同。

结构与用法

导入 sys/types.hsys/ipc.h 以使用此函数。

key_t ftok(char *fname, int id)

参数

  1. 已存在文件的路径;
  2. 用户指定的一个整数,常用 ASCII 字符。

返回值

若运行成功,该函数返回唯一 id;其他情况返回 -1。

控制信号量
int semctl(int __semid, int __semnum, int __cmd, ...)

参数

  1. 目标信号量集的标识符,该信号量集必须存在;

  2. [仅对单个信号量操作时需要] 信号量在信号量集中的索引值,相当于指定信号量;

  3. 要运行的操作,可用操作为:

    • 对单个信号量操作:
      • GETVAL 获取指定信号量的当前值;
      • SETVAL 设置指定信号量的数量,需要在可变参数中写入 int 值;
      • GETPID 获取最后一次操作该信号量的进程号;
      • GETNCNT 获取正在等待该信号量值增加事件的进程数量;
      • GETZCNT 获取正在等待该信号量值归0事件的进程数量;
    • 对整个信号量操作:
      • IPC_RMID 删除信号量集,删除后再次使用将返回 EIDRM
      • IPC_STAT 获取信号量集元信息,需要在可变参数中写入 struct semid_ds* buf
      • IPC_SET 修改信号量集的权限,需要在可变参数中写入 struct semid_ds* buf
  4. 可变参数

    需要操作的参数则必须填入,其他操作可填入 NULL。

返回值

运行成功时,视操作而返回对应值;对于 SETVALIPC_RMIDIPC_SET,返回0;其他情况返回-1并设置 errno 值。

PV 操作
int semop(int __semid, struct sembuf *__sops, size_t __nsops)

参数

  1. 目标信号量集的标识符,该信号量集必须存在;
  2. 存有信号量操作信息的 sembuf 结构体指针,传入指针是因为可能需要操作结构体数组,以同时操作多个信号量,结构体见下方 NOTE 注记;
  3. 要操作的信号量个数,需要和结构体个数一致,不能多于参数2中结构体数组数量;由 OS 决定的操作的信号量不能大于32个;如果此项为0,函数之间返回0,不会进行任何操作。

返回值

运行成功后,返回0;其他情况返回-1并设置 errno 值。

Info

sembuf 结构体

struct sembuf {
    unsigned short sem_num;  // 信号量在集合中的索引(从 0 开始)
    short          sem_op;   // 操作类型(见下文)
    short          sem_flg;  // 操作标志(如 IPC_NOWAIT、SEM_UNDO)
};
  • sem_op 对信号量值的增减或等待事件:
    • 若大于0,表示归还此信号量的数量,信号量的值将对应增加;
    • 若小于0,表示请求此信号量的数量,信号量的值将对应减少,如果当前此信号量的值不够减,该进程可能等待;
    • 若为0,表示该进程想要等待此信号量的值变为0。
  • sem_flg 信号量操作的行为:
    • IPC_NOWAIT 停用等待,如果不能全部得到请求信号量,或者此时信号量不为0,立刻返回-1并设置 errnoEAGAIN Macro;
    • SEM_UNDO 启用回退机制,如果当前进程非正常退出,退回已请求信号量以避免死锁。

共享内存

需要导入 sys/shm.h

多个进程间能使用相同的一段物理内存进行数据交换,应注意访问互斥。

// TODO

消息队列 (System V)

本节使用 System V 库。

将每条消息视为队列的一个链表项目,并存放于内核空间。

创建消息队列
int msgget(key_t __key, int __msgflg)

参数

  1. 消息队列唯一标签键值,用于识别唯一的消息队列,常用 ftok() 生成;
  2. 创建时选项,可用的选项:

返回值

如果运行成功,返回消息队列标识符;其他情况返回-1并设置 errno 值。

向队列发送数据
int msgsnd(int __msqid, const void *__msgp, size_t __msgsz, int __msgflg)

参数

  1. 消息队列的ID,消息队列必须存在;
  2. 指向需要传输的目标数据起始地址的指针;
  3. 目标数据的大小;
  4. 发送时选项,可用的选项: IPC_NOWAIT 消息队列满时,不进行等待,立刻返回-1并设置 errno 值。

返回值

如果运行成功,返回0;其他情况返回-1并设置 errno 值。

从队列接收数据
ssize_t msgrcv(int __msqid, void *__msgp, size_t __msgsz, long __msgtyp, int __msgflg)

参数

  1. 消息队列的ID,消息队列必须存在;

  2. 指向接收数据的内存起址指针;

  3. 数据部分大小,传入的数据段第一个项目必须为 long 类型,用于消息队列区分消息类型,且不包括消息类型的大小

  4. 要接收的消息类型值,视取值而设定范围: 若为0,读取队列的第1条消息;

    若小于0,读取队列中小于等于 |__msgtyp| 中,拥有最小值的第1条消息;

    若大于0,读取队列中为 __msgtyp 的第1条消息。

  5. 读取队列的选项,可用选项: IPC_NOWAIT 如果没有符合条件的消息,不进行等待,立刻返回-1并设置 errnoENOMSG

    MSG_NOERR 读取到的消息项目若长于指定的长度,截断后段消息而不返回-1和 E2BIG

    [仅 Linux] MSG_EXCEPT 读取非 __msgtyp 的第1条消息。

返回值

如果运行成功,返回读取的字节数;其他情况返回-1并设置 errno 值。

7 操作系统服务

7.1 时间与日期

取得当前时间与日期

需要导入 time.h

time_t timestamp;
struct tm* local_time;
char formatted_time[20];
// Get timestamp
time(&timestamp);
// Get localtime and date by timestamp
local_time = localtime(&timestamp);
// Get formatted time date string
strftime(formatted_time, sizeof(formatted_time), "%Y/%m/%d %H:%M:%S", local_time);

7.2 运行系统指令

需要导入 stdlib.h

在 Linux 下,调用 /bin/sh 运行指定的 Shell 指令,这会建立一个子进程,并在指令运行完成后才使父进程继续运行。

int system(const char *__command)

参数

要运行的 Shell 指令。

返回值

如果指令成功运行,返回指令运行状态;如果传入 NULL,返回非零值表示 Shell 可用。其他情况返回-1.

指令运行状态解析与使用 wait() 函数的解析方法一致。

8 分区

文件系统是管理数据在物理设备上的存储结构并向上提供基本操作封装的程序。更现代的文件系统提供更多扩展功能,例如日志记录、碎片整理、访问控制。

文件系统设置在分区内的头部,这被称为超级块,它存储文件系统需要的分区全局信息,例如分区大小、块大小(在 Windows,这称为簇)、索引节点数量、空闲块数量。不同的分区(在 Windows,这称为逻辑硬盘)可设置不同的文件系统。

文件系统隐藏物理设备的存储地址细节,向上提供可读方式访问文件与目录。

分区是物理硬盘的数个分割出来的物理区域,目前主流的分区类型是 GPT (GUID Partition Table,全局唯一标识符分区表),适用于旧版硬件的分区类型是 MBR (Master Boot Record,主引导记录)。

Ext. 示例程序段

2进程*单个信号量*制作与吃掉馅饼

此段代码使用 System V 方法进行信号量操作。

Info

提示:此段代码中:

  • 资源种类数 = 信号量种类数,一种资源对应一种信号量;不同种信号量使用特定索引访问,索引值从0开始。例如:鸡蛋、黄油、砂糖就是不同种资源;

  • 指定资源数量 = 指定信号量的数量,一种资源可拥有数个。例如:有3块黄油。

#include "sys/types.h"
#include "sys/ipc.h"
#include "sys/sem.h"
#include "stdio.h"
#include "unistd.h"
 
int main() {
    // 创建信号量集              创建唯一标识, 资源种类数, 选项与权限设置
    //                              V        V           V
    int semaphore_set = semget(ftok(".", 1), 2, IPC_CREAT | 0666);
    // 信号量集管理     资源索引  操作  可选参数,这里指要设置0号资源的数量
    //                    V    V     V
    semctl(semaphore_set, 0, SETVAL, 1);
 
    int sub_pid = fork();
    if (sub_pid < 0) {
        printf("Cannot create subprocess : %s\n", strerror(errno));
        return 1;
    }
    
    if (sub_pid > 0) {
        // 子进程可运行至此
        while (1) {
            // 创建 sembuf 结构体,以便发送给 semop() 操作信号量集
            //                      资源索引, 数值与操作, 其他选项
            //                            \   V  ┌-------┘
            struct sembuf sem_operation = {0, 1, 0}; /* 准备制作1个馅饼 */
            
            // 在指定的信号量集中,进行 sembuf 指定的操作
            //        这里可认为是传入结构体数组    数组数量
            //                         V         V
            semop(semaphore_set, &sem_operation, 1); /* 制作1个馅饼 */
            
            // 取得指定信号量的数量
            //                          此值不使用  操作
            //                                V    V
            int count = semctl(semaphore_set, 0, GETVAL);
            printf("Produced a pancake! %d\n", count);
            sleep(1);
            
            if (count >= 5) {
                printf("Production finished.\n");
                printf("Waiting for subprocess eating...\n");
                int sub_exit_status;
                wait(&sub_exit_status);
                return 0;
            }
        }
    }
    if (sub_pid == 0) {
        // 主进程可运行至此
        while (1) {
            struct sembuf sem_operation = {0, -1, 0}; /* 准备吃掉1个馅饼 */
            semop(semaphore_set, &sem_operation, 1); /* 吃掉1个馅饼 */
 
            int count = semctl(semaphore_set, 0, GETVAL);
            printf("Eating a pancake! %d\n", count);
            sleep(2);
 
            if (count == 0) {
                printf("Eating finished!\n");
                return 0;
            }
        }
    }
    return 0;
}

2进程*命名管道*单向发送字符串

#include "universal.h"
#include "sys/wait.h"
 
#define WRITE 1
#define READ 0
 
int main() {
    int status = mkfifo("/tmp/fifo_test_c", 0666);
    if (status < 0) {
        printf("Cannot create FIFO : %s\n", strerror(errno));
    }
    int sub_pid = fork();
    if (sub_pid < 0) {
        printf("Cannot create subprocess : %s\n", strerror(errno));
        return 1;
    }
    if (sub_pid > 0) {
        char* data = "I love the feeling of lying on the grass slope, staring at star blinking in the sky";
        int write_pipe_fd = open("/tmp/fifo_test_c", O_WRONLY);
        printf("Preparing for data...\n");
        sleep(3);
        write(write_pipe_fd, data, strlen(data));
        printf("Sending finished.\n");
 
        close(write_pipe_fd);
        printf("Waiting for subprocess exit...\n");
        wait(NULL);
        status = unlink("/tmp/fifo_test_c");
        if (status == 0) {
            printf("Removed FIFO pipe file.\n");
        }
        else {
            printf("WARNING: cannot remove fifo file : %s\n", strerror(errno));
        }
        return 0;
    }
    if (sub_pid == 0) {
        char* receiver = malloc(84);
        int read_pipe_fd = open("/tmp/fifo_test_c", O_RDONLY);
        read(read_pipe_fd, receiver, 84);
        printf("Received: %s\n", receiver);
        
        close(read_pipe_fd);
        free(receiver);
        return 0;
    }
}

2进程*消息队列*发送结构体

主进程建立消息队列,然后分次发送2个 MsgPackage 结构体数据;子进程从此消息队列读取 MSG_TYPE 为2的消息并显示,然后删除消息队列。

#include "sys/types.h"
#include "sys/ipc.h"
#include "sys/msg.h"
 
#include "stdio.h"
#include "stdlib.h"
#include "string.h"
#include "errno.h"
#include "fcntl.h"
#include "unistd.h"
#include "sys/wait.h"
 
typedef struct MsgPackage {
    long MSG_TYPE;
    int identity;
    char comment[64];
} MsgPackage;
 
int main() {
    printf("[Main] Creating message queue...\n");
    int msg_queue_id = msgget(ftok(".", 1), IPC_CREAT | 0666);
    if (msg_queue_id < 0) {
        printf("Cannot create message queue : %s\n", strerror(errno));
        return 1;
    }
 
    int sub_pid = fork();
    if (sub_pid < 0) {
        printf("Cannot create subprocess : %s\n", strerror(errno));
        return 1;
    }
    if (sub_pid > 0) {
        printf("[Main] Created subprocess\n");
        printf("[Main] Preparing for message package...\n");
        MsgPackage msg_pkg_list = {
           .MSG_TYPE = 1,
           .identity = 1
        };
        strcpy(msg_pkg_list.comment, "Welcome to Mexico!");
 
        int status = msgsnd(msg_queue_id, &msg_pkg_list, sizeof(msg_pkg_list) - sizeof(long), 0);
        if (status < 0) {
            printf("Cannot send to message queue : %s\n", strerror(errno));
            return 1;
        }
 
        msg_pkg_list.MSG_TYPE = 2;
        msg_pkg_list.identity = 2;
        strcpy(msg_pkg_list.comment, "It's a grand carnival!");
        status = msgsnd(msg_queue_id, &msg_pkg_list, sizeof(msg_pkg_list) - sizeof(long), 0);
        if (status < 0) {
            printf("Cannot send to message queue : %s\n", strerror(errno));
            return 1;
        }
        printf("[Main] Successfully send to message queue\n");
        
        printf("[Main] Waiting for subprocess end...\n");
        int ret_status;
        wait(&ret_status);
        if (ret_status != 0) {
            printf("[Main] Subprocess seems getting some problems\n");
            return 1;
        }
        return 0;
    }
 
    if (sub_pid == 0) {
        MsgPackage msg_pkg_list[2];
        memset(msg_pkg_list, 0, sizeof(msg_pkg_list));
        printf("[Sub] Ready for receiving data...\n");
        ssize_t byte_count = msgrcv(msg_queue_id, msg_pkg_list, sizeof(MsgPackage) - sizeof(long), 2, 0);
        printf("[Sub] Received data, parsing result:\n");
        if (msg_pkg_list != NULL)
            printf("[%d] %s\n", msg_pkg_list[0].identity, msg_pkg_list[0].comment);
        if ((msg_pkg_list + 1)->identity != 0)
            printf("[%d] %s\n", msg_pkg_list[1].identity, msg_pkg_list[1].comment);
        printf("Byte count: %ld\n", byte_count);
 
        int status = msgctl(msg_queue_id, IPC_RMID, NULL);
        if (status  < 0) {
            printf("[Sub] Cannot delete message queue : %s\n", strerror(errno));
            return 1;
        }
        printf("[Sub] Message queue deleted\n");
        return 0;
    }
}