读linux programming interface chapter 1-2

读linux programming interface chapter 1-2

Linux内核通常位于路径/boot/vmlinuz或类似的地方。早期的UNIX实现中内核被称为unix。后来支持虚拟内存的UNIX实现将内核重命名为vmunix。Linux上z替换了最后的x,表示内核是一个压缩的可执行文件

用了EFI的系统就不一样了,UEFI 规范对启动文件格式有特定的要求(如使用 .efi 后缀)

EFI/nixos/71caf08ri3h650s37i6fk7xyyqhmfhf8-linux-6.6.6-bzImage.efi
EFI/nixos/fnv6snx15cszf813z99313jwqvyard49-initrd-linux-6.6.5-initrd.efi
EFI/nixos/hsfcdf322wbncd0cddqqk9gvp0k3mxr7-linux-6.6.5-bzImage.efi
EFI/nixos/y6ssha6hr1s42zw8g1qwdhmn4zx0wfqc-initrd-linux-6.6.6-initrd.efi

以 bzImage.efi 结尾,这通常表示它们是Linux内核的压缩映像。在Linux中,bzImage(即“Big zImage”)是一种特殊格式的内核映像,用于引导Linux系统


虚拟内存管理优点:

  • 进程彼此隔离,与内核隔离,这样一个进程不能读取或修改另一个进程或内核的内存
  • 只有进程的一部分需要保留在内存中,从而降低了每个进程的内存要求,并允许更多的进程同时保存在RAM中。这增加了CPU的利用率,因为它增加了在任何给定时间,至少有一个进程可以被CPU执行的可能性

MMU 使每个进程都拥有自己的虚拟地址空间。对于进程来说,它似乎拥有一整块连续的内存区域,但实际上这些虚拟地址可能被映射到物理内存的不同区域,甚至可能被换入换出到磁盘

全是MMU的功劳,MMU还负责把内核内存和用户内存分隔开

侧信道攻击:某些高级攻击,如缓存侧信道攻击(例如Spectre和Meltdown),可以利用硬件的特性来绕过常规的安全隔离机制,尽管这些攻击通常需要复杂的技术和特定的条件


内核层面文件通常被抽象为一个连续的字节流,使用lseek()系统调用随机访问

“随机访问”是相对于“顺序访问”来说的。顺序访问只能按照一定的顺序(通常是从头到尾)访问文件。随机访问允许你直接跳转到文件中的任何位置进行读写操作,lseek() 提供的随机访问能力,允许用户在这些字节流上进行灵活的位置移动操作

许多应用程序和库将换行字符(十进制ASCII代码10)解释为终止一行文本并开始另一行。UNIX系统没有文件结束字符;通过返回没有数据的读取来检测文件的结尾

为了执行文件I/O,C程序通常使用标准C库中包含的I/O函数。这组函数被称为stdio库,包括fopen()、fclose()、scanf()、printf()、fgets()、fputs()等。stdio函数基于I/O系统调用(如open()、close()、read()、write()等)之上

系统调用通常由内核提供,并非用 C 语言直接实现。内核代码负责处理与硬件相关的操作,在更低的级别上执行任务

系统调用的接口可能用 C 语言写成,以便用户空间的程序能调用它们,但实际的功能实现是在内核中,通常涉及到与硬件的直接交互

为了让程序员在 C 语言中方便地使用这些服务,操作系统开发者会用 C 语言编写一组函数,这些函数构成了系统调用的接口


子进程继承了父进程数据、stack和heap的副本,可以独立地修改这些副本(放置在标记为只读的内存中的程序文本由两个进程共享)

子进程继续在与父进程相同的代码中执行不同的函数,或经常使用execve()系统调用加载执行全新的程序。execve()调用销毁现有的文本、数据、stack和heap,根据新程序的代码替换它们


进程通过两种方式终止

  • _exit()系统调用(或相关的exit()库函数)请求自己的终止
  • 被信号杀死

任何情况下进程会产生一个终止状态:一个小的非负整数值,父进程可使用wait()系统调用查看它。调用_exit()的情况下进程明确指定自己的终止状态。如果进程被信号杀死,终止状态根据导致进程死亡的信号类型设置


每个进程都有一些关联的用户ID(UIDs)和组ID(GIDs):

  • 实际用户ID和实际组ID:这些标识进程所属的用户和组。新进程从其父进程继承这些ID。登录shell从系统密码文件中的相应字段获取其实际用户ID和实际组ID
  • 有效用户ID和有效组ID:这两个ID(结合补充组ID)用于确定进程在访问受保护的资源(如文件和进程间通信对象)时的权限。通常,进程的有效ID与相应的实际ID具有相同的值。更改有效ID是允许进程假定其他用户或组的特权的机制
  • 补充组ID:这些ID标识进程所属的其他组。新进程从其父进程继承其补充组ID。登录shell从系统组文件获取其补充组ID

What the heck of this???


UNIX系统上特权进程是其有效用户ID为0(超级用户)的进程。这样的进程绕过内核通常应用的权限限制。非特权进程有非零的有效用户ID,并必须遵守内核的权限规则

从2.2的内核开始Linux将传统授予超级用户的权限划分为一组称为权限能力(capabilities)的独立单元。每个特权操作都与某个特定capability关联,进程只有在拥有相应capability时才能执行操作。传统的超级用户进程对应于启用了所有权限能力的进程

启动系统时内核创建了特殊的init进程,是所有进程的父进程,它源自/sbin/init。系统上的所有进程都是由init或其后代使用fork()创建的。init进程始终具有进程ID 1,以超级用户权限运行。init进程不能被杀死(即使是超级用户),只有在系统关闭时才终止。init的主要任务是创建并监视运行系统所需的一系列进程


守护进程(daemon)由系统以与其他进程相同的方式创建和处理,具有以下特点:

  • 长寿命。守护进程通常在系统启动时启动,并在系统关闭时一直存在
  • 在后台运行,且没有控制终端,它可以读取输入或写入输出

daemon例子

  • syslogd,在系统日志中记录消息
  • httpd,通过HTTP提供网页

C程序可使用外部变量char **environ访问环境,各种库函数允许进程检索和修改环境中的值

进程会消耗资源,如打开的文件、内存和CPU时间。setrlimit()系统调用可以为其消耗的各种资源设定上限。每个资源限制有两个关联的值:

  • 软限制,限制进程可能消耗的资源量
  • 硬限制,是软限制可能调整到的值的上限

无特权的进程可能会将其某个特定资源的软限制更改为从零到相应的硬限制之间的任何值,但只能降低其硬限制

使用fork()创建新进程时,会继承其父进程的资源限制设置

使用ulimit调整shell的资源限制。这些限制设置会被shell创建的子进程继承,用于执行命令

ulimit是shell built-in,是用户级别的,只影响本shell会话,可以控制很多资源限制上限

 (><)  ulimit -a
real-time non-blocking time  (microseconds, -R) unlimited
core file size              (blocks, -c) unlimited
data seg size               (kbytes, -d) unlimited
scheduling priority                 (-e) 0
file size                   (blocks, -f) unlimited
pending signals                     (-i) 127195
max locked memory           (kbytes, -l) 8192
max memory size             (kbytes, -m) unlimited
open files                          (-n) 1024
pipe size                (512 bytes, -p) 8
POSIX message queues         (bytes, -q) 819200
real-time priority                  (-r) 0
stack size                  (kbytes, -s) 8192
cpu time                   (seconds, -t) unlimited
max user processes                  (-u) 127195
virtual memory              (kbytes, -v) unlimited
file locks                          (-x) unlimited

mmap()系统调用在调用进程的虚拟地址空间中创建一个新的内存映射

映射分为:

  • 文件映射: 将文件的一个区域映射到调用进程的虚拟内存中。可通过在相应内存区域的字节上进行操作来访问文件内容。根据需要映射的页面会自动从文件中加载
  • 匿名映射: 没有相应的文件。这种类型的映射通常用于分配一块新的内存区域,如作为替代传统的 malloccalloc 。分配的内存页面会被自动初始化为0 (内存区域中的内容被初始化为0)

os通常以“页面”(page)为单位管理内存。页面是内存中的一个固定大小的区域,在许多系统中一个页面的大小可能是 4KB

进程的映射中的内存可能与其他进程共享。可能因为两个进程映射了文件的相同区域,或是因为由fork()创建的子进程从其父进程继承了映射

两个或更多进程共享相同页面时,每个进程可能会看到其他进程对页面内容所做的更改,具体取决于映射是作为私有还是共享创建的。

  • 私有映射: 修改映射内容对其他进程不可见,且不传递到底层文件
  • 共享映射:修改映射内容对共享相同映射的其他进程可见,且传递到底层文件

内存映射有多种用途

  • 从可执行文件的相应段初始化进程的文本Text段
  • 分配新的(填充为零的)内存
  • 文件I/O(内存映射I/O)
  • 进程间通信(通过共享映射)
#include <sys/mman.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
 
int main() {
    int fd;
    char *mapped;
 
    /* 打开文件 */
    fd = open("example.txt", O_RDWR);
    if (fd < 0) {
        perror("open");
        return 1;
    }
 
    /* 将文件映射到内存 */
    mapped = mmap(NULL, 4096, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    if (mapped == MAP_FAILED) {
        perror("mmap");
        close(fd);
        return 1;
    }
 
    /* 通过内存写入数据 */
    strcpy(mapped, "Hello, mmap!");
 
    /* 解除映射和关闭文件 */
    munmap(mapped, 4096);
    close(fd);
 
    return 0;
}

静态库是早期UNIX系统上唯一的库类型。是已编译的对象模块的结构化捆绑包。要使用静态库中的函数,在构建程序的链接命令中指定该库。链接器在将主程序的各种函数引用解析到静态库中的模块之后,从库中提取所需的对象模块的副本,将这些副本复制到生成的可执行文件中

  • 在不同的可执行文件中复制对象代码浪费磁盘空间
  • 当同时执行使用相同库函数的静态链接的程序时,出现内存浪费,每个程序需要其自己的函数副本驻留在内存中
  • 如果库函数需要修改,在重新编译该函数并将其添加到静态库之后,所有需要使用更新的函数的应用程序都必须重新与库链接

如果程序链接到共享库,链接器只写入一个记录到可执行文件,指示在运行时可执行文件需要使用该共享库

当可执行文件在运行时加载到内存中时,动态链接器确保找到并加载所需的共享库到内存,并执行运行时链接,将可执行文件中的函数调用解析为共享库中的相应定义

运行时共享库的代码只需要一个副本驻留在内存中;所有正在运行的程序都可以使用该副本


进程间通信(IPC)机制:

  • 信号: 指示事件已发生
  • pipes 和 FIFOs: 在进程之间传输数据
  • socket: 从一个进程传输数据到另一个进程,无论在同一主机上还是通过网络连接的不同主机上
  • 文件锁定: 允许进程锁定文件的区域,防止其他进程读取或更新文件内容
  • 消息队列,用于在进程间交换消息(数据包)
  • 信号量,用于同步进程的行为
  • 共享内存,允许两个或多个进程共享一块内存。当一个进程更改共享内存的内容时,所有其他进程可以看到更改

进程接收到信号时:

  • 忽略信号
  • 被信号杀死
  • 被暂停,直到稍后收到信号后恢复

对于大多数信号类型,程序可以忽略信号,或建立信号处理程序,而不是接受默认信号操作。信号处理程序是程序员定义的函数,当信号被传递到进程时自动调用

在生成和传递之间的时间间隔内,信号对于进程来说被认为是待处理的。通常待处理的信号会在下次计划运行接收进程时立即被传递,或者如果进程已经在运行,则立即被传递。但也可以通过将其添加到进程的信号掩码来阻止信号。如果在阻塞信号时生成了一个信号,它将保持挂起状态,直到稍后被解除阻塞(即从信号掩码中删除)


session是进程的集合。一个session中的所有进程有相同的会话标识符。session leader是创建session的进程,其进程ID成为会话ID

session主要由job-control shell使用。由job-control shell创建的所有进程组都属于与shell相同的session,而shell是session leader

session通常有一个关联的控制终端。当session leader进程首次打开终端设备时,就会建立控制终端。对于交互式shell创建的session,这是用户登录的终端。一个终端最多只能是一个session的控制终端

由于打开控制终端,session leader成为终端的控制进程。如果发生终端断开(例如,关闭终端窗口),控制进程会收到一个SIGHUP信号

session中的一个进程组是前台进程组(前台任务),它可以从终端读取输入并发送输出。如果用户在控制终端上键入中断字符(Control-C)或挂起字符(Control-Z),则终端驱动程序发送一个信号,该信号杀死或挂起(即停止)前台进程组。session可以有任意数量的后台进程组(后台任务),这些任务是通过用和字符(&)结束命令创建的

感觉对这个的理解完全不够


伪终端(Pseudo Terminal PTY)是一对连接的虚拟设备,称为主设备(PTY master)和从设备(PTY slave)。这对设备提供了一个IPC通道,允许数据在两个设备间双向传输

主设备通常由终端仿真程序(如终端模拟器)控制,而从设备则提供了类似于真实终端的接口,供应用程序(如 shell、文本编辑器等)使用

控制程序(连接到 PTY master)和面向终端的程序(连接到 PTY slave)之间的交互模拟了传统的物理终端用户与终端程序之间的交互。控制程序充当用户的角色,输入数据并接收输出,而面向终端的程序则认为它在与一个普通的终端交互。这种机制使得远程控制、终端仿真等操作成为可能

伪终端用于

  • 终端仿真:X窗口系统登录下提供的终端窗口的实现
  • 远程绘画:telnet, ssh

现代 Unix 系统中,伪终端通常通过动态分配实现,其中主设备可以动态地创建和销毁从设备,以适应不同的终端会话需求。

伪终端是一种重要的软件机制,它允许非物理终端环境(如远程会话或图形界面下的终端仿真器)模拟物理终端的行为和功能


/proc是虚拟文件系统,以文件和目录的形式提供对内核数据结构的接口。这提供了简单的机制来查看和更改各种系统属性

/proc/PID中PID是进程ID,可查看系统上运行的每个进程的信息

/proc文件内容通常以便于人类阅读的文本形式存在,可被shell脚本解析