i-stat怎么打印void foo(int a[10]) {}void foo(int a[]) {}void foo(int* a) {}struct Student { … };void bar(struct Student s) { … }int main(void) {int a[10] = { … };fo

新闻资讯2026-04-20 23:47:40

通过size命令查看一个可执行程序的代码区、数据区和BSS区的大小。
每个进程的用户空间都拥有独立的从虚拟内存到物理内存的映射,谓之进程间的内存壁垒。
代码:vm.c
3.内存的分配与释放
malloc/calloc/realloc/free
|
v
brk/sbrk
|
v
mmap/munmap
|
v
kmalloc/kfree
以增加方式分配或释放虚拟内存
分配:映射+占有
| ____________
在地址空间(虚拟内存)和
存储空间(物理内存)之间 指定内存空
建立映射关系 间的归属性
释放:放弃占有+解除映射
| |
解除对内存空 消除地址空间(虚拟内存)和存储
间的归属约束 空间(物理内存)之间的映射关系
#include <unistd.h>
void* sbrk(intptr_t increment);
堆顶->- - - - - - - -
堆区
-----------------
sbrk(10)
堆顶->- - - - - - - -
10字节
- - - - - - - -<-返回值
堆区
-----------------
sbrk(-10)
- - - - - - - -<-返回值
10字节
堆顶->- - - - - - - -
堆区
-----------------
成功返回调用该函数之前的堆顶指针,失败返回-1。
increment

0 - 堆顶指针上移,增大堆空间,分配虚拟内存
<0 - 堆顶指针下移,缩小堆空间,释放虚拟内存
=0 - 不分配也不释放虚拟内存,仅仅返回当前堆顶指针
系统内核维护一个指针,指向堆内存的顶端,即有效堆内存中最后一个字节的下一个位置。sbrk函数根据增量参数increment调整该指针的位置,同时返回该指针原来的位置,期间若发生内存耗尽或空闲,则自动追加或取消相应内存页的映射。
123____~~~~~~~~…
^ ^ ^ ^
堆顶
代码:sbrk.c
以绝对地址的方式分配或释放虚拟内存
int brk(void* end_data_segment);
成功返回0,失败返回-1。
end_data_segment
当前堆顶,分配虚拟内存
<当前堆顶,释放虚拟内存
=当前堆顶,空操作
堆顶->- - - - - - - -<-void* p = sbrk(0);
堆区
-----------------
brk(p+10)
- - - - - - - -<-p+10
10字节
堆顶->- - - - - - - -
堆区
-----------------
brk§
堆顶->- - - - - - - -<-p
堆区
-----------------
系统内核维护一个指针,指向当前堆顶,brk函数根据指针参数end_data_segment设置堆顶的新位置,期间若发生内存耗尽或空闲,则自动追加或取消相应内存页的映射。
代码:brk.c
建立虚拟内存到物理内存或文件的映射
#include <sys/mman.h>
void* mmap(void* start, size_t length, int prot,
int flags, int fd, off_t offset);
成功返回映射区虚拟内存的起始地址,失败返回MAP_FAILED(void类型的-1)。
start - 映射区虚拟内存的起始地址,NULL表示自动选择
length - 映射区的字节数,自动按页取整
prot - 访问权限,可取以下值:
PROT_READ - 可读
PROT_WRITE - 可写
PROT_EXEC - 可执行
PROT_NONE - 不可访问
flags - 映射标志,可取以下值:
MAP_ANONYMOUS - 匿名映射,将虚拟内存映射到物理内存,函数的最后两个参数fd和offset被忽略
MAP_PRIVATE - 私有映射,将虚拟内存映射到文件的内存缓冲区中而非磁盘文件
MAP_SHARED - 共享映射,将虚拟内存映射到磁盘文件中
MAP_DENYWRITE - 拒写映射,文件中被映射区域不能存在其它写入操作
MAP_FIXED - 固定映射,若在start上无法创建映射,则失败(无此标志系统会自动调整)
MAP_LOCKED - 锁定映射,禁止被换出到换页文件
fd - 文件描述符
offset - 文件偏移量,自动按页对齐
解除虚拟内存到物理内存或文件的映射
int munmap(void
start, size_t length);
成功返回0,失败返回-1。
start - 映射区的起始地址
length - 映射区的字节数
代码:mmap.c
八、系统调用
应用程序--------------+
vi/emacs/gftp/firefox |
| |
标准库、第三方库 |
C/C++/Qt/X11 |
| |
系统调用<------------+
brk/sbrk/mmap/munmap
1.Linux系统内核提供了一套用于实现各种系统功能的子程序,谓之系统调用。程序编写者可以象调用普通C语言函数一样调用这些系统调用函数,以访问系统内核提供的各种服务。
2.系统调用函数在形式上与普通C语言函数并无差别。二者的不同之处在于,前者工作在内核态,而后者工作在用户态。
3.在Intel的CPU上运行代码分为四个安全级别:Ring0、Ring1、Ring2和Ring3。Linux系统只使用了Ring0和Ring3。用户代码工作在Ring3级,而内核代码工作在Ring0级。一般而言用户代码无法访问Ring0级的资源,除非借助系统调用,使用户代码得以进入Ring0级,使用系统内核提供的功能。
4.系统内核内部维护一张全局表sys_call_table,表中的每个条目记录着每个系统调用在内核代码中的实现入口地址。
5.当用户代码调用某个系统调用函数时,该函数会先将参数压入堆栈,将系统调用标识存入eax寄存器,然后通过int 80h指令触发80h中断。
6.这时程序便从用户态(Ring3)进入内核态(Ring0)。
7.工作系统内核中的中断处理函数被调用,80h中断的处理函数名为system_call,该函数先从堆栈中取出参数,再从eax寄存器中取出系统调用标识,然后再从sys_call_table表中找到与该系统调用标识相对应的实现代码入口地址,挈其参数调用该实现,并将处理结果逐层返回到用户代码中。
九、文件
1.文件系统的物理结构
1)硬盘的物理结构:驱动臂、盘片、主轴、磁头、控制器
2)磁表面存储器的读写原理
硬盘片的表面覆盖着薄薄的磁性涂层,涂层中含有无数微小的磁性颗粒,谓之磁畴。相邻的若干磁畴组成一个磁性存储元,以其剩磁的极性表示二进制数字0和1。为磁头的写线圈中施加脉冲电流,可把一位二进制数组转换为磁性存储元的剩磁极性。利用磁电变换,通过磁头的读线圈,可将磁性存储元的剩磁极性转换为相应的电信号,表示二进制数。
3)磁道和扇区
磁盘旋转,磁头固定,每个磁头都会在盘片表面画出一个圆形轨迹。改变磁头位置,可以形成若干大小不等的同心圆,这些同心圆就叫做磁道(Track)。每张盘片的每个表面上都有成千上万个磁道。一个磁道,以512字节为单位,分成若干个区域,其中的每个区域就叫做一个扇区(Sector)。扇区是文件存储的基本单位。
4)柱面、柱面组、分区和磁盘驱动器
硬盘中,不同盘片相同半径的磁道所组成的圆柱称为柱面(Cylinder)。整个硬盘的柱面数与每张盘片的磁道数相等。
硬盘上的每个字节需要通过以下参数定位:
磁头号:确定哪个盘面
柱面号:确定哪个磁道 > 磁盘I/O
扇区号:确定哪个区域 /
偏移量:确定扇区内的位置
若干个连续的柱面构成一个柱面组
若干个连续的柱面组构成一个分区
每个分区都建有独立的文件系统
若干分区构成一个磁盘驱动器
2.文件系统的逻辑结构
磁盘驱动器:| 分区 | 分区 | 分区 |
分区:| 引导块 | 超级块 | 柱面组 | 柱面组 | 柱面组 |
柱面组:
| 引导块 | 柱面组 | i节点映 | 块位图 | i节点表 | 数据块集 |
| 副 本 | 信 息 | 射 表 | | | |
i节点号:431479
i节点
文件元数据
100 | 200 | 300
根据目录文件中记录的i节点编号检索i节点映射表,获得i节点下标,用该下标查i节点表,获得i节点,i节点中包含了数据块索引表,利用数据块索引从数据块集中读取数据块,即获得文件数据。
直接块:存储文件实际数据内容
间接块:存储下级文件数据块索引表
100


xxx

xxx

pathname - 文件路径
flags - 状态标志,可取以下值:
O_RDONLY - 只读
O_WRONLY - 只写 > 只选其一
O_RDWR - 读写 /
O_APPEND - 追加
O_CREAT - 创建,不存在即创建,已存在即打开,除非与以下两个标志之一合用,由此标志mode参数才有效。
O_EXCL - 排它,已存在即失败
O_TRUNC - 清空,已存在即清空,同时有O_WRONLY或O_RDWR
O_SYNC - 写同步,在数据被写到磁盘之前写操作不会完成,读操作本来就是同步的,此标志对读操作没有意义
O_ASYNC - 异步,在文件可读写时产生一个SIGIO信号,在对信号的处理过程中读写I/O就绪的文件,只能用于终端设备或网络套接字,而不能用于磁盘文件
O_NONBLOCK - 非阻塞,读操作不会因为无数据可读而阻塞,写操作也不会因为缓冲区满而阻塞,相反会返回失败,并设置特定的errno
mode - 权限模式,三位八进制:0XXX
__/ | _ / |
拥有者用户 同组用户 其它用户
4: 可读
2: 可写
1: 可执行
所创建文件的实际权限除了跟mode参数有关,还受权限掩码的影响。
mode=0666
umask=0002
权限=mode&~umask=0664
创建新文件
int creat(const char* pathname, mode_t mode);
flags: O_WRONLY | O_CREAT | O_TRUNC
打开已有文件
int open(const char* pathname, int flags);
关闭文件
int close(int fd);
成功返回0,失败返回-1。
fd - 文件描述符
代码:open.c
作为文件描述符表项在文件描述符表中的下标,合法的文件描述符一定是大于或等于0的整数。每次产生新的文件描述符表项,系统总是从下标0开始在文件描述符表中寻找最小的未使用项。每关闭一个文件描述符,无论被其索引的文件表项和v节点是否被删除,与之对应的文件描述符表项一定会被标记为未使用,并在后续操作中为新的文件描述符所占用。系统内核缺省为每个进程打开三个文件描述符:
#include <unistd.h>
#define STDIN_FILENO 0 // 标准输入,即键盘
#define STDOUT_FILENO 1 // 标准输出,终端屏幕,有缓冲
#define STDERR_FILENO 2 // 标准错误,终端屏幕,无缓冲
UC C C++
标准输入 0 stdin cin
标准输出 1 stdout cout
标准错误 2 stderr cerr
数据类型 int FILE* iostream
文件描述符是用户程序和系统内核关于文件的唯一联系方式。
5.文件的读取和写入
向指定文件写入字节流
ssize_t write(int fd, const void* buf, size_t count);
成功返回实际写入的字节数(0表示未写入),失败返回-1。
fd - 文件描述符
buf - 内存缓冲区
count - 期望写入的字节数
ssize_t read(int fd, void* buf, size_t count);
成功返回实际读取的字节数(0表示读到文件尾),失败返回-1。
ABCDEFGHIJLMNOPQ
4
1)ABCD
2)EFGH
3)IJLM
4)NOPQ
5)->0
fd - 文件描述符
buf - 内存缓冲区
count - 期望读取的字节数
代码:write.c、read.c
基于系统调用的文件读写本来就是面向二进制字节流的,因此对二进制读写而言,无需做任何额外的工作。如果要求文件中内容必须是可阅读的,那么就必须通过格式化和文本解析处理二进制形式的数据和文本字符串之间的转换。
代码:binary.c、text.c
6.顺序与随机读写
ABCdef
^ ^ ^
0 3 6
每个打开的文件都有一个与其相关的文件读写位置保存在文件表项中,用以记录从文件头开始计算的字节偏移。文件读写位置通常是一个非负的整数,用off_t类型表示,在32位系统上被定义为long int,而在64位系统上则被定义为long long int。打开一个文件时,除非指定了O_APPEND标志,否则文件读写位置一律被设为0,即文件首字节的位置。每一次读写操作都从当前的文件读写位置开始,并根据所读写的字节数,同步增加文件读写位置,为下一次读写做好准备。因为文件读写位置是保存在文件表项而不是v节点中的,因此通过多次打开同一个文件得到多个文件描述符,各自拥有各自的文件读写位置。
人为调整文件读写位置
off_t lseek(int fd, off_t offset, int whence);
成功返回调整后的文件读写位置,失败返回-1。
fd - 文件描述符
offset - 文件读写位置相对于whence参数的偏移量
whence
SEEK_SET - 从文件开始
SEEK_CUR - 从当前位置开始
SEEK_END - 从文件尾开始
lseek函数仅仅是修改文件表项中的文件读写位置,并不引发实际的I/O操作,速度很快。
lseek(fd, 10, SEEK_SET);
lseek(fd, -10, SEEK_END);
lseek(fd, 0, SEEK_CUR); // 返回当前读写位置
lseek(fd, 0, SEEK_END); // 返回文件总字节数
lseek(fd, -10, SEEK_SET); // 错误
lseek(fd, 10, SEEK_END); // 允许,空洞部分补0
代码:seek.c
7.标准I/O和系统I/O
应用程序----------+
| |
v |
标准(库)I/O |
fopen/fwrite/fclose |
| |
v |
系统(库)I/O |
open/write/close <–+
代码:stdio.c、sysio.c
标准库通过缓冲区优化,减少系统调用的次数,降低在用户态和内核态之间来回切换的频率,提高运行速度,缩短运行时间。
8.复制文件描述符(表项)
进程表项

文件描述符表
|文件描述符标志 | 文件表项指针 | 0
|文件描述符标志 | 文件表项指针 | 1
|文件描述符标志 | 文件表项指针 | 2
int fd = open(…); // fd: 3
进程表项

文件描述符表
|文件描述符标志 | 文件表项指针 | 0
|文件描述符标志 | 文件表项指针 | 1
|文件描述符标志 | 文件表项指针 | 2
|文件描述符标志 | 文件表项指针 | 3 -> 文件表项
int dup(int oldfd);
成功返回目标文件描述符,失败返回-1。
oldfd - 源文件描述符
int fd2 = dup(fd); // fd2: 7
进程表项

文件描述符表
|文件描述符标志 | 文件表项指针 | 0
|文件描述符标志 | 文件表项指针 | 1
|文件描述符标志 | 文件表项指针 | 2
|文件描述符标志 | 文件表项指针 | 3->文件表项->v节点
… ^
|文件描述符标志 | 文件表项指针 | 7 --------+
fd2(7)和fd(3)对应同一个文件表项,访问同一个文件。
dup函数将oldfd参数所对应的文件描述符表项复制到文件描述符表第一个空闲项中,同时返回该表项所对应的文件描述符。
close(fd);
close(fd2);
int dup2(int oldfd, int newfd);
成功返回目标文件描述符,失败返回-1。
oldfd - 源文件描述符
newfd - 目标文件描述符
dup2函数在复制oldfd参数所标识的源文件描述符表项时,会首先检查由newfd参数所标识的目标文件描述符表项是否空闲,若空闲则直接将前者复制给后者,否则会先将目标文件描述符newfd关闭,再行复制。
fd1 = open(“1.txt”, …); --> 文件表项
> v节点
fd2 = open(“1.txt”, …); --> 文件表项 /

VSZ: 占用虚拟内存大小(KB)
RSS: 占用半导体物理内存大小(KB)
TTY: 终端次设备号
ttyn - 物理终端(硬件设备)
pts/n - 虚拟终端(软件窗口)
? - 无控制终端,如后台进程
STAT/S: 进程状态
O - 就绪,等待被调度
R - 运行,Linux下没有O状态,就绪状态也用R表示
S - 可唤醒睡眠,系统中断,获得资源,收到信号,
都可被唤醒,转入运行状态
D - 不可唤醒的睡眠
只能被wake_up系统调用唤醒
T - 暂停,收到SIGSTOP(19)信号转入暂停状态,
收到SIGCONT(18)信号转入运行状态
W - 等待内存分页(2.6内核后被废弃)
X - 终止且被回收,不可见
Z - 僵尸,已退出但未被回收
< - 高优先级
N - 低优先级
L - 有被锁定在半导体内存中的分页
s - 会话首进程
l - 多线程化
+ - 在前台进程组中
START: 进程启动时间
TIME: 进程运行时间
COMMAND/CMD: 进程启动命令
F: 进程标志
1 - 通过fork产生的子进程,但是
并没有通过exec创建新进程
4 - 拥有超级用户(root)特权
PPID: 父进程的PID
NI: 进程nice值,-20~19,进程优先级浮动量
PRI: 进程优先级=80+nice,60~99,值越小优先级越高
I/O消耗型进程,奖励,提高优先级,降低nice值
处理机消耗型进程,惩罚,降低优先级,提高nice值
ADDR: 内核进程的内存地址,普通进程显示"-"
SZ: 占用虚拟内存页数
WCHAN: 进程正在等待的内核函数或事件
PSR: 进程当前正在被哪个处理器执行
4.父子孤尸
父进程创建子进程,子进程继承父进程。
一个父进程可以创建多个子进程,每个子进程有且仅有一个父进程,除非是根进程(PID=0,调度器实例)没有父进程。
进程树:
调度进程(PID=0)
init(PID=1)
xinetd
in.telnetd <- 远程登录
login <- 用户名和口令
bash <- Shell命令:ls
ls -> 显示目录条目清单
父进程在创建完子进程以后依然存在,甚至可以和子进程进行某种形式的交互,如:传参、回收、通信等。
旧进程在创建完新进程以后被其取代,新进程沿用旧进程的PID,继续独立地存在。
父进程创建子进程以后,子进程在操作系统的调度下与其父进程同时运行。如果父进程先用子进程的终止而终止,子进程即成为孤儿进程,同时被init进程收养,即成为init进程的子进程,因此init进程又被成为孤儿院进程。一个进程成为孤儿进程是正常的,系统中大多数守护进程都是孤儿进程。
如果子进程先于父进程的终止而终止,但父进程由于某种原因,没有回收子进程的尸体(终止状态),子进程即成为僵尸进程。僵尸进程虽然已经不再活动,但其终止状态和PID仍然被保留,也会占用系统资源,直到其被父进程或init进程回收为止。如果父进程直到其终止都没有回收其处于僵尸状态的子进程,init进程会立即回收这些僵尸。因此一个进程不可能同时既是僵尸进程又是孤儿进程。
5.进程的各种ID
系统内核会为每个进程维护一个进程表项,其中包括如下ID:
进程ID:系统为每个进程分配的唯一标识。内核在分配进程ID时,会持续增加,直到不发在增加了,再从头寻找被释放的ID,即延迟重用。

100

200 X

300

400 X

500
501

200
父进程ID:父进程的PID,在创建子进程的过程中被初始化到子进程的进程表项中。
实际用户ID:启动该进程的用户ID。
实际组ID:启动该进程的用户组ID。
有效用户ID:通常情况下,取自进程的实际用户ID。如果该进程的可执行文件带有设置用户ID位,那么该进程的有效用户ID就取自其可执行文件的拥有者用户ID。
有效组ID:通常情况下,取自进程的实际组ID。如果该进程的可执行文件带有设置组ID位,那么该进程的有效组ID就取自其可执行文件的拥有者组ID。
一个进程的能力和权限,由其有效用户ID和有效组ID决定。
#include <unistd.h>
pid_t getpid(void); // 返回调用进程的PID
pid_t getppid(void); // 返回调用进程的PPID,
// 即其父进程的PID
uid_t getuid(void); // 返回调用进程的实际用户ID
uid_t getgid(void); // 返回调用进程的实际组ID
uid_t geteuid(void); // 返回调用进程的有效用户ID
uid_t getegid(void); // 返回调用进程的有效组ID
代码:id.c
6.创建子进程
产生进程分支(fork)
pid_t fork(void);
成功分别父子进程中返回子进程的PID和0,失败返回-1。
调用一次返回两次:
在父进程中返回所创建子进程的PID,而在子进程中返回0
函数的调用者往往可以根据该函数返回值的不同,分别为父子进程编写不同的处理分支
pid_t pid = fork();
if (pid == -1) {
perror(“fork”);
exit(EXIT_FAILURE);
}
if (pid == 0) {
子进程的处理分支
exit(EXIT_SUCCESS);
}
父进程的处理分支
exit(EXIT_SUCCESS);
子进程是父进程不完全副本,子进程的数据区、BSS区、堆栈区(包括I/O缓冲区),甚至命令行参数和全景变量区都从父进程拷贝,唯有代码区与父进程共享。
fork函数成功返回以后,父子进程各自独立地运行,其被调度的先后顺序并不确定,某些实现可以保证子进程先被调度。
fork函数成功返回以后,系统内核为父进程维护的文件描述符表也被复制到子进程的进程表项中,文件表项并不复制。
父进程的进程表项
文件描述符表

7 | … | * -----------> 文件表项(读写位置)
| fork ^
v |
子进程的进程表项 |
文件描述符表 |
… |
7 | … | * ------------------+
系统总线程数达到上限(/proc/sys/kernel/threads-max)或用户总进程数达到上限(ulimit -u),fork函数将返回失败。
一个进程如果希望创建自己的副本并执行同一份代码,或希望与另一个进程并发地运行,都可以使用fork函数。
(父)进程
|
__^__fork
/
父进程 子进程
_____/
|
代码共享
数据复制

 (父)进程
       |
   __^__fork/vfork
  /        
  |     子进程

pid_t waitpid(pid_t pid, int* status, int options);
成功返回所回收子进程的PID,失败返回-1。
pid - 进程标识,可取以下值:
<-1: 等待并回收由-pid所标识的进程组中任意子进程
-1: 等待并回收任意子进程,相当于wait函数
0: 等待并回收与调用进程同组的任意子进程

0: 等待并回收由pid所标识的特定子进程
status - 输出子进程的终止状态,可置NULL。
options - 选项,可取以下值:
0: 阻塞模式,等不来就死等,类似于wait函数
WNOHANG: 非阻塞模式,所等子进程仍在运行,则返回0
代码:waitpid1.c、waitpid2.c
10.创建新进程
子进程:父子同在——并行。
新进程:以新换旧——取代。
exec函数族包括6个函数,根据参数的形式和是否使用PATH环境变量进行区分。
为一个函数传递不定数量的字符串参数:
void foo(const char* arg, …); // 变长参数表
foo(NULL);
foo(“abc”, NULL);
foo(“abc”, “def”, NULL);
foo(“abc”, “def”, …, NULL);
void bar(const char* arg[]); // 字符指针数组
const char* a[] = {NULL};
bar(a);
const char* a[] = {“abc”, NULL};
bar(a);
const char* a[] = {“abc”, “def”, NULL};
bar(a);
const char* a[] = {“abc”, “def”, …, NULL};
bar(a);
#include <unistd.h>
int execl(const char* path, const char* arg, …);
/ _________________/
/ |
可执行文件的路径 命令行参数
$ gcc hello.c -o hello
gcc->argv[0]
hello.c->argv[1]
-o->argv[2]
hello->argv[3]
execl("/usr/bin/gcc", “gcc”, “hello.c”, “-o”, “hello”,
NULL); | | | |
argv[0] argv[1] argv[2] argv[3]
用arg, …作为命令行参数,运行path所表示的可执行文件,创建新进程,并用新进程取代调用进程。成功不返回,失败返回-1。
int execlp(const char* file, const char* arg, …);
通过file参数传入可执行文件的名字即可,无需带路径,该函数会遍历PATH环境变量中的所有路径,寻找可执行文件。
execlp(“gcc”, “gcc”, “hello.c”, “-o”, “hello”, NULL);
int execle(const char* path, const char* arg, …,
char* const envp[]);
|
环境变量
以NULL结尾的字符指针数组
int execv(const char* path, char* const argv[]);
char* const a[] = {“gcc”, “hello.c”, “-o”, “hello”, NULL} | | | |
argv[0] argv[1] argv[2] argv[3]
execv("/usr/bin/gcc", a);
int execvp(const char* file, char* const argv[]);
int execve(const char* path, char* const argv[],
char* const envp[]);
exec(执行)+
l - list,以变长参数表的形式传入命令行参数
p - path,使用PATH环境变量寻找可执行文件
e - environ,以字符指针数组的形式传入环境变量
v - vector,以字符指针数组的形式传入命令行参数
代码:argenv.c、exec.c
与fork或vfork函数不同,exec函数不是创建调用进程的子进程,而是创建一个新的进程取代调用进程自身。新进程会用自己的全部地址空间,覆盖调用进程的地址空间,但进程的PID保持不变。调用exec函数不仅改变调用进程的地址空间和进程映像,调用进程的一些属性也发生了变化:
1)任何处于阻塞状态的信号都会丢失;
2)被设置为捕获的信号会还原为默认操作;
3)有关线程属性的设置会还原为缺省值;
4)有关进程的统计信息会复位;
5)与进程内存有关的任何数据都会丢失,包括内存映射文件;
6)标注库在用户空间维护的一切数据结构,如通过malloc函数族动态分配的堆内存,通过atexit/on_exit函数注册的退出处理函数等,都会丢失。
但有些属性会被新进程继承下来,比如PID、PPID、实际用户ID和实际组ID、优先级,以及文件描述符(除非该文件描述符带有FD_CLOEXEC标志位)等。
vfork+exec模式:
调用exec函数固然可以创建出新的进程,但是新进程会取代原来的进程。如果既想创建新的进程,同时有希望原来的进程继续存在,则可以考虑使用vfork+exec模式,即在由vfork产生的子进程中调用exec函数,新进程取代了子进程,但父进程依然存在。
代码:ve1.c、ve2.c
如果一个进程可以根据用户的输入创建不同的进程,并在所建
进程结束以后继续重复这个过程,那么这个进程就是Shell:
Shell进程(bash)
±>显示提示符并
| 等待用户输入
| | <- 输入:ls -l
| v
| 调用vfork函
| 数创建子进程 ---------------> 子进程(ls)
| | |
| v v
| 调用waitpid函数 根据用户的输入调用exec
| 等待并回收子进程 <------ 函数创建新进程(/bin/ls) | |
±--------+
#include <stdlib.h>
int system(const char* command);
成功返回command命令行进程的终止状态,失败返回-1。
command - 命令行字符串
如果调用vfork或waitpid函数出错,返回-1。
如果调用exec函数出错,返回127。
如果都成功,返回command进程的终止状态,由waitpid函数的status参数输出。
如果command参数取NULL指针,该函数返回-1表示失败,返回其它非0(真)值表示当前Shell可用,返回0(假)表示不可用。
代码:sys1.c、sys2.c
system=vfork+exec+waitpid
十一、信号
1.基本概念
1)什么是信号?
事件(信号)
过程(进程)1---------v--------->
/
/事件
过程(进程)2 /—v---
/
/
过程(进程)3 /— ---
信号是提供异步事件处理机制的软件中断。这些异步事件可能来自硬件设备,也可能来自系统内核,甚至可能来自用户程序。进程之间可以相互发送信号,这使信号成为一种进程间通信(Inter-Process Communication, IPC)的基本手段。信号的异步特性不仅表现为它的产生是异步的,对它的处理同样也是异步的。程序设计者不可能也不需要精确地预见什么时候触发什么信号,也同样无法预见该信号究竟在什么时候会被处理。一切尽在内核操控下异步地发生。
2)什么是信号处理?
每一个信号都有其生命周期:
产生:信号被生成,并被发送至系统内核
未决:信号被内核缓存,而后被递送至目标进程
递送:内核已将信号发送至目标进程
忽略 - 什么也不做。
捕获 - 暂定当前的执行过程,转而调用一个事先写好
的信号处理函数,待该函数完成并返回后,再
继续之前被中断的过程。
默认 - 既不忽略该信号,也不用自己定义处理方式,
而是按照系统默认的方式予以响应。
激励(信号)->响应(信号处理)
3)信号的名称和编号
信号名称:形如SIGXXX的字符串或宏定义,提高可读性。
信号编号:整数
通过kill -l命令查看当前系统所支持的全部信号名称及其编号。
1~31,31个不可靠信号,也叫非实时信号。
34~64, 31个可靠信号,也叫实时信号。
共62个信号,注意没有32和33信号。
SIGHUP(1),控制终端关闭,终止
SIGINT(2),用户产生中断符(Ctrl+C),终止
SIGQUIT(3),用户产生退出符(Ctrl+),终止+转储
SIGBUS(7),硬件或内存对齐错误,终止+转储
SIGKILL(9),不能被捕获和忽略,终止
SIGSEGV(11),无效内存访问,终止+转储
SIGPIPE(13),向读端已关闭的管道写入,终止
SIGALRM(14),alarm函数设置的闹钟到期,终止
SIGTERM(15),可被捕获和忽略,终止
SIGCHLD(17),子进程终止,忽略
SIGIO(29),异步I/O事件,终止
代码:loop.c
2.捕获信号
#include <signal.h>
typedef void (*sighandler_t) (int);
|
函数指针
指向一个接受整型参数且无返回值的函数
设置针对特定信号的处理方式,即捕获特定的信号:
sighandler_t signal(int signum, sighandler_t handler);
成功返回原信号处理方式,失败返回SIG_ERR(sighandler_t类型的-1)。
signum - 信号编号
handler - 信号处理函数指针,也可以取以下值:
SIG_IGN - 忽略信号
SIG_DFL - 默认操作

// 定义信号处理函数
void sigint (int signum) {
SIGINT(2)信号的处理代码
}

// 捕获SIGINT(2)信号
if (signal(SIGINT, sigint) == SIG_ERR) {
perror(“signal”);
return -1;
}

SIGINT(2)
v
SIGINT(2)/PID/sigint->系统内核
v
目标进程中的sigint函数
signal(SIGINT, SIG_DFL); // 按默认方式处理
signal(SIGINT, SIG_IGN); // 忽略信号
当一个信号正在被处理的过程中,相同的信号再次产生,该信号会被阻塞,直到前一个信号处理完成,即从信号处理函数中返回,后一个被阻塞的信号才被递送,进而再次执行信号处理函数。当一个不可靠信号正在被处理的过程中,多个相同的信号再次产生,只有第一个信号会被阻塞,其它信号直接丢弃,如果是可靠信号,都会被阻塞,并按照产生的顺序依次被递送。
信号处理函数及被其调用的函数都有可能发生重入,由此可能引发无可预知的风险。
global = 0;

++global;

所有标准I/O函数都是不可重入函数。在信号处理的过程中要慎用。
A信号->A信号处理函数 打印AAAAAA
printf(调试信息);
B信号->B信号处理函数 / 打印BBBBBB
AAABBBBBBAAA
代码:signal.c
3.信号捕获流程
中断
主控制流程--------- v ----------->
/
信号处理函数 / ------> 用户空间
/ / ---------
/ / 内核空间
内核处理流程-----------> ---------->
do_signal system_call
handle_signal sys_sigreturn
setup_frame restore_sigcontext
信号的本质是一个中断的处理过程,而非多线程的并发过程。
线程安全的函数未必是可重入函数。


锁机制 局部化
4.信号捕获的一次性问题
在某些非Linux操作系统上,存在信号捕获的一次性问题:
即使设置了对某个信号的捕获,只有设置后的第一个该信号被递送时,信号处理函数会被执行,以后再来相同的信号,均按默认方式处理。如果希望对信号的捕获具有持久性,可以在信号处理函数返回前再次设置对该信号的捕获。
5.太平间信号
通过SIGCHLD(17)信号高效地回收子进程僵尸。
高效:及时性,适时性。
代码:sigchld.c
6.信号处理的继承与恢复
1)fork/vfork函数创建的子进程会继承父进程的信号处理方式,直到子进程调用exec函数创建新进程替代其自身为止。
2)exec函数创建的新进程会将原进程中被设置为捕获的信号还原为默认处理。在原进程中被忽略的信号于新进程中继续被忽略。
代码:fork.c
7.发送信号
1)通过键盘向当前拥有控制终端的前台进程发送信号
Ctrl+C -> SIGINT(2),默认终止进程
Ctrl+ -> SIGQUIT(3),默认终止进程且转储
Ctrl+Z -> SIGTSTP(20),默认停止(挂起)进程
2)来自硬件或者内核的错误和异常引发的信号
SIGILL(4),进程试图执行非法指令
SIGBUS(7),硬件或总线对齐错误
SIGFPE(8),浮点异常
SIGSEGV(11),无效内存访问
SIGPIPE(13),向无读端的管道写入
SIGSTKFLT(16),浮点数协处理器栈错误
SIGXFSZ(25),文件资源超限
SIGPWR(30),断电
SIGSYS(31),无效系统调用
3)通过kill命令发送信号
kill [-信号] PIDs
|
缺省发送SIGTERM(15)信号
超级用户可以给任何进程发信号,普通用户只能给自己的进程发信号。
4)调用函数发送信号
向特定的进程(组)发送信号:
int kill(pid_t pid, int signum);
成功(至少发出去一个信号)返回0,失败返回-1。
pid - 进程(组)标识,可取以下值:
<-1:向-pid进程组中的所有进程发送信号
-1: 向系统中的所有进程发送信号
0: 向调用进程同组的所有进程发送信号

0: 向进程标识为pid的特定进程发送信号
signum - 信号编号,取0用于检查pid进程是否存在,如果不存在,kill函数会返回-1,且置errno为ESRCH。
代码:kill.c
向调用进程自己发送信号:
int raise(int signum);
成功返回0,失败返回-1。
signum - 信号编号
raise(SIGINT);
kill(getpid(), SIGINT);
代码:raise.c
通过raise或kill向调用进程发送信号,如果该信号被捕获,则要等到信号处理函数返回后,这两个函数才会返回。
8.暂停、睡眠和闹钟
暂停,即不受时间限制的睡眠:
int pause(void);
成功阻塞,失败返回-1。
该函数使调用进程进入无时限的睡眠状态,即不参与内核调度,直到有信号终止了调用进程或被捕获。如果有信号被调用进程捕获,当信号处理函数返回以后,pause函数才会返回,且返回值为-1,同时置errno为EINTR,表示阻塞的系统调用被信号中断。pause函数要么不返回,要么返回-1,不会返回0。
代码:pause.c
受时间限制的睡眠:
unsigned int sleep(unsigned int seconds);
返回0或剩余秒数。
seconds - 以秒为单位的睡眠时限
该函数使调用进程睡眠seconds秒,除非有信号终止了调用进程或被其捕获。如果有信号被调用进程捕获,在信号函数返回以后,sleep函数才会返回,且返回值为剩余秒数,否则该函数返回0,表示睡眠充足。
代码:sleep.c
int usleep(useconds_t usec);
睡够了返回0,睡不够返回-1,同时置errno为EINTR。
usec - 为微秒为单位的睡眠时限
1微秒=10^-6秒
Intel CPU:50~55毫秒
设置闹钟
unsigned int alarm(unsigned int seconds);
返回0或先前闹钟的剩余时间。
seconds - 以秒为单位的闹钟时间
alarm函数使系统内核在该函数被调用以后seconds秒的时候,向调用进程发送SIGALRM(14)信号。若在调用该函数前已设过闹钟且尚未到期,则该函数会重设闹钟,并返回先前所设闹钟的剩余秒数,否则返回0。若seconds参数取0,则取消之前设置过且未到期的闹钟。
代码:alarm.c
通过alarm函数所设置的定时只是一次性的,即在定时到期时发行一次SIGALRM(14)信号,此后不会再发送该信号。如果希望获得周期性的定时效果,可以在SIGALRM(14)信号的处理函数中继续调用alarm函数,完成下一个定时的设置。
代码:clock.c
9.信号集
#include <signal.h>
typedef __sigset_t sigset_t;
#include <sigset.h>
typedef struct {
unsigned long int __val[_SIGSET_NWORDS];
32 x 32 = 1024位
} __sigset_t;
#define _SIGSET_NWORDS
(1024 / (8 * sizeof(unsigned long int)))
|<-1024位->|


        4 3 2 1

9.基于管道通信的特殊情况
1)从写端已被关闭的管道中读取
只要管道缓冲区中还有数据,依然可被正常读取,一直读到缓冲区空,这时read函数会返回0(既不是返回-1也不是阻塞),如同读到文件尾。
2)向读端已被关闭的管道中写入
会直接触发SIGPIPE(13)信号。该信号的默认操作是终止执行写入动作的进程。但如果执行写入动作的进程已经事先将SIGPIPE(13)信号设置为忽略或捕获,这时虽然进程不会因为写入无读端的管道而被终止,但write函数会返回-1,并置errno为EPIPE。
3)在/usr/include/linux/limits.h头文件中定义的PIPE_BUF宏(4096)表示管道写缓冲区的大小。如果写管道时发现缓冲区中的空闲空间不足以容纳此次write调用所要写入的字节数,则write函数会阻塞,直到缓冲区中的空闲空间变得足够大为止。如果同时有多个进程向同一个管道写入数据,而每次调用write函数写入的字节数都不大于BUF_SIZE,则这些write操作不会互相穿插(原子化,atomic),反之单次写入的字节数超过了BUF_SIZE,则它们的write操作可能会相互穿插。读取一个缓冲区为空的管道,只要其写端没有被关闭,读操作就会阻塞,除非该读文件描述符被设置为非阻塞(O_NONBLOCK),此时会立即返回失败,并置errno为EAGAIN。
10.管道符号的原理
命令1 | 命令2 | 命令3 …
| | |
输出>管道>输入
输出>管道>输入
输出>…
A | B
Shell进程:
int pipefd[2];
pipe(pipefd); // 创建无名管道
vfork(); // 产生一个子进程1
子进程1:
close(pipefd[0]); // 关闭无名管道的读端
dup2(pipefd[1], STDOUT_FILENO); // 写端=标准输出
exec(A) // 创建A进程,继承了原进程的文件描述符表
// 在A进程中,写端=标准输出
// printf/puts将数据写入无名管道的写端
vfork(); // 产生一个子进程2
子进程2:
close(pipefd[1]); // 关闭无名管道的写端
dup2(pipefd[0], STDIN_FILENO); // 读端=标准输入
exec(B) // 创建B进程,继承了原进程的文件描述符表
// 在B进程集中,读端=标准输入
// scanf/gets从无名管道的读端读取数据
A和B就成为协作进程,A写入数据进管道,B从管道中读取A写入的数据。
代码:output.c、intput.c、shell.c
11.XSI的IPC对象
1)IPC对象的标识符(ID)和键(KEY)
IPC对象在系统内核中的唯一名称用键(KEY)表示。不同的进程可以通过键来引用该IPC对象。一旦进程获得了该IPC对象,即通过其标识(ID)来称谓该对象。
IPC对象(键) 内核空间
— / — | — ---------------
/键 |键 键 用户空间
进程1 进程2 进程3
ID1 ID2 ID3
#include <sys/ipc.h>
key_t ftok(const char* pathname, int proj_id);
成功返回IPC对象的键,失败返回-1。
pathname - 一个真实存在的路径,使用该路径的i节点号
proj_id - 项目ID,仅低8为有效,-128127/0255
相同项目使用相同的pathname和proj_id,保证key的一致性
不同项目使用不同的pathname或proj_id,避免key发生冲突
2)IPC对象的编程接口
A.创建或获取IPC对象
// 共享内存
int shmget(key_t key, size_t size, int shmflg);
// 消息队列
int msgget(key_t key, int msgflg);
// 信号量集
int semget(key_t key, int nsems, int semflg);
B.控制或销毁IPC对象
// 共享内存
int shmctl(int shmid, int cmd, struct shmid_ds* buf);
// 消息队列
int msgctl(int msqid, int cmd, struct msqid_ds* buf);
// 信号量集
int semctl(int semid, int semnum, int cmd,
union semun arg);
cmd - 控制命令,可取以下值:
IPC_STAT: 获取IPC对象的属性
IPC_SET: 设置IPC对象的属性
IPC_RMID: 删除IPC对象
C.IPC对象的权限结构
struct ipc_perm {
key_t __key; // 键
uid_t uid; // 拥有者用户
gid_t gid; // 拥有者组
uid_t cuid; // 创建者用户
gid_t cgid; // 创建者组
unsigned short mode; // 权限
unsigned short __seq; // 序号
};
其中只有uid、gid和model三个字段可以在创建完成以后被修改。
3)共享内存
两个或者更多进程,共享一块由系统内核负责维护的物理内存,其地址空间通常被映射到每个进程虚拟内存堆和栈之间的不同区域。
共享内存的属性结构:
struct shmid_ds {
struct ipc_perm shm_prem; // 权限结构
size_t shm_segsz; // 字节数
time_t shm_atime; // 最后加载时间
time_t shm_dtime; // 最后卸载时间
time_t shm_ctime; // 最后改变时间
pid_t shm_cpid; // 创建进程的PID
pit_t shm_lpid; // 最后加(卸)载进程的PID
shmatt_t shm_nattch; // 当前加载计数

};
int shmget(key_t key, size_t size, int shmflg);
size - 共享内存的字节数,按页向上取整。获取已有共享内存对象时可置0。
shmflg - 创建标志
0: 获取,不存在即失败。
IPC_CREATE - 创建兼获取,不存在即创建,已存在直接获取
IPC_EXCL - 不存在即创建,已存在直接报错。
加载共享内存到虚拟内存,家里虚拟内存和物理内存间的映射
void* shmat(int shmid, const void* shmaddr,
int shmflags);
成功返回共享内存起始地址,失败返回void类型的-1。
shmid - 共享内存标识
shmaddr - 共享内存起始地址,置NULL由系统内核选择
shmflags - 加载标志,可取以下值:
0: 可读可写
SHM_RDONLY: 只读
SHM_RND: 若shmaddr非空且不是页边界,则将其调整至页边界。
卸载共享内存
int shmdt(const void
shmaddr);
成功返回0,失败返回-1。
shmat函数负责将给定共享内存映射到调用进程的虚拟内存空间,返回映射区的起始地址,同时将系统内核中共享内存对象的加载计数(shm_nattch)加1。调用进程在获得shmat函数返回的共享内存起始地址以后,就可以象访问普通内存一样访问该共享内存中的数据。shmdt函数负责从调用进程的虚拟内存中解除shmaddr所指向的映射区到共享内存的映射,同时将系统内核中共享内存对象的加载计数(shm_nattch)减1。因此加载计数为0的共享内存必定是没有任何进程使用的。shmctl(…,
IPC_RMID, …)调用可以用于销毁共享内存,但并非真的销毁,而只是做一个销毁标记,禁止任何进程对该共享内存形成新的加载,但已有的加载依然保留。只有当其使用者们纷纷卸载,直至其加载计数降为0时,共享内存才会真的被销毁。
代码:wshm.c、rshm.c
通过共享内存实现进程间通信,可以直接访问由系统内核维护的公共内存区域,不需要额外构建用户缓冲区,也不需要在用户缓冲区和内核缓冲区之间来回复制数据。因此共享内存是速度最快的进程间通信机制。但是共享内存因为缺乏必要的同步机制,往往需要借助其它进程间通信策略提供某种形式的停等机制。
4)消息队列
消息队列是由单个的类型各异的一系列消息结构组成的链表。
消息 ±> 消息
--------- | ----------
消息类型 | 消息类型
数据长度 | 数据长度
消息数据 | 消息数据
消息指针–+ 消息指针–>
尾 首
->2|1|2|3|1|2|2|1|3|3->
^ ^ ^^
最大可发送消息字节数:8192(8K)
最大全队列消息字节数:16384(16K)
最大全系统消息队列数:16
最大全系统消息总个数:262144
#include <sys/msg.h>
int msgget(key_t key, int msgflg);
|
0/IPC_CREAT/IPC_EXCL
发送消息
int msgsnd(
int msqid, // 消息队列标识
const void* msgp, // ->| 消息类型(4字节) | 消息数据 |
size_t msgsz, // <–长度–>
int msgflg // 0阻塞/IPC_NOWAIT非阻塞
);
成功返回0,失败返回-1。
msgsnd函数的msgp参数所指向的内存中包含4个字节大小的消息类型,其值必须大于0,但该函数的msgsz参数所表示的期望发送字节数却不包含消息类型所占的4个字节。
如果系统内核中的消息未达上限,则msgsnd函数会将欲发送消息加入消息队列并立即返回0,否则该函数会阻塞,直到系统内核允许加入新消息为止(比如有消息因被接收而离开消息队列)。若msgflg参数中包含IPC_NOWAIT位,则msgsnd函数在系统内核中的消息已达上限的情况下不会阻塞,而是返回-1,并置errno为EAGAIN。
接收消息
int msgrcv(
int msqid, // 消息队列标识
void* msgp, // ->| 消息类型(4字节) | 消息数据缓冲区 |
size_t msgsz, // <-----长度----->
long msgtyp, // 消息类型
int msgflg // 0阻塞/IPC_NOWAIT非阻塞/…
);
成功返回实际接收到的消息数据的长度,失败返回-1。
msgtyp可取以下值:
0 - 提取消息队列中的第一条消息而无论其类型。

0 - msgflg不含MSG_EXCEPT位,提取第一条类型为msgtyp的消息,msgflg含MSG_EXCEPT位,提取第一条类型不为msgtyp的消息。
<0 - 提取队列中类型小于或等于|msgtyp|的消息,类型越小的越先被提取。
注意msgrcv函数的msgp参数所指向的内存块中包含4字节的消息类型,其值由该函数输出,但该函数的msgsz参数所表示的期望接收字节数以及该函数所返回的实际接收字节数都不包含消息类型4个字节。
若存在与msgtyp参数匹配的消息,但是数据长度大于msgsz参数,且msgflg参数包含MSG_NOERROR位,则置截取该消息数据的前msgsz字节返回,剩余部分直接丢弃;但如果msgflg参数不包含MSG_NOERROR位,则不处理该消息,直接返回-1,并置errno为E2BIG。
msgrcv函数根据msgtyp参数对消息队列中消息有选择地接收,只有满足条件的消息才会被复制到应用程序缓冲区并从内核中删除。如果满足msgtyp条件的消息不只一条,则按照先进先出的规则提取。
若消息队列中有可接收消息,则msgrcv函数会将该消息移出消息队列,并立即返回所接收到的消息数据字节数,表示接收成功,否则此函数会阻塞,直到消息队列中有可接收消息为止。若msgflg参数包含IPC_NOWAIT位,则msgrcv函数在消息队列中没有可接收消息的情况下不会阻塞,而是返回-1,并置errno为ENOMSG。
代码:wmsg.c、rmsg.c
5)信号量集
资源的需求者多于资源本身,如何协调有限资源在多数需求者之间的分配,以使每个资源需求者都有相对均衡的几率获得其所要求的资源。
系统内核中为每一种资源维护一个资源计数器,其值为当前空闲资源的数量,每当一个进程试图获取该种资源时,会先尝试减少其计数器的值,如果该计数器的值够减(计数器>=0),则说明空闲资源足够,该进程即获得资源,如果该计数器的值不够减,则说明空闲资源不够,该进程即进入等待模式,等候其它拥有该种资源的进程释放资源。任何一个拥有该种资源的进程,一旦决定释放该资源,都必须将其计数器的值予以增加,以表示空闲资源量的增加,为其它等候该资源的进程提供条件。
用一个信号量表示一种类型资源,其值为该类型资源的空闲数量。用由多个信号量组成的信号量集表示多种类型的资源。
信 / 信号量0->《三国演义》=2
号 / 信号量1->《水浒传》=4
量 信号量2->《红楼梦》=11
集 信号量3->《四游记》=8
创建或获取信号量集
#include <sys/sem.h>
int semget(key_t key, int nsems, int semflg);
成功返回信号量集标识符,失败返回-1。
key - 信号量集键
nsems - 信号量个数,即资源的种类数
semflg - 创建标志,可取以下值:
0 - 获取,不存在即失败
IPC_CREAT - 创建,不存在即创建,已存在即获取
IPC_EXCL - 排斥,已存在即失败
操作信号量集:减操作->拥有资源…释放资源->加操作
int semop(int semid, struct sembuf* sops,
unsigned nsops);
成功返回0,失败返回-1。
semid - 信号量集标识
sops - 操作结构数组
nsops - 操作结构数组长度
struct sembuf {
unsigned short sem_num; // 信号量编号(集合索引)
short sem_op; // 操作数(-获取/+释放)
short sem_flg; // 操作标志
// (0/IPC_NOWAIT)
};
sops指针指向一个struct sembuf类型的结构体数组,其中每个元素都是一个struct sembuf类型的结构体,该结构体包含三个字段,用于表示针对信号量集中的一个特定信号量的特定操作。
semid=0 semid=0
---------- --semop()-> ----------
0: 15 -1 0: 14
1: 21 1: 21
2: 33 +1 2: 34
3: 42 -1 3: 41
sops


传输层:源到目的地的点对点传输
网络层:路径选择、路由、寻址等网络结构拓扑
数据链路层:物理寻址、数据通道、错误检测等通信路径
物理层:在数据和电平信号之间进行转换
比喻:
买点心(应用层:业务需求)
选择包装(表示层:数据形式)
选择快递公司(会话层:通信过程)
填写收寄单(传输层:点对点)
选择路径(网络层:通路)
中途周转(数据链路层:多点连线)
送货过程(物理层:实际通信)
4)TCP/IP协议栈
传输层:TCP、UDP
网络层:IP、ICMP、IGMP
链路层:ARP、RARP
5)消息包和数据流
应用层:HTTP请求=用户数据包
传输层:TCP头+用户数据包=TCP包
网络层:IP头+TCP包=IP包
链路层:以太网头+IP包+以太网尾=以太网帧
物理层:以太网帧->电平信号
| ^
v |
传输线路
发送数据流:消息自协议栈顶层向底层流动,逐层打包。
接收数据流:消息自协议栈底层向顶层流程,逐层解析。
6)IP地址
IP地址(Internet Protocol Address,互联网协议地址)是一种地址格式,为互联网上的每个网络和主机分配一个逻辑地址,其目的是消除物理地址的差异性。
IP地址的计算机内部用一个网络字节序的32位(4个字节)无符号整数表示。通常习惯将其表示为点分十进制整数字符串的形式。例如:
点分十进制整数字符串:1.2.3.4
32位(4个字节)无符号整数:0x01020304
内存布局:| 0x01 | 0x02 | 0x03 | 0x04 |
低地址--------------->高地址
网络字节序就是大端字节序,高位在低地址,低位在高地址。
中国北京市东城区珠市口大街珍贝大厦三层第一教室
一台计算机的IP地址=网络地址+主机地址
A级地址:以0为首8位网络地址+24位主机地址
B级地址:以10为首16位网络地址+16位主机地址
C级地址:以110为首24位网络地址+8位主机地址
D级地址:以1110为首的32为多(组)播地址
例如:某台计算机的IP地址为192.168.182.48,其网络地址和主机地址分别为何?
192 168 182 48
11000000 10101000 10110110 00110000
以110为首,C级地址,网络地址是192.168.182.0,主机地址是48。
主机IP地址 & 子网掩码 = 网络地址
主机IP地址 & ~子网掩码 = 主机地址
例如:主机IP地址192.168.182.48,子网掩码255.255.255.0,其网络地址和主机地址分别为何?
192.168.182.48 & 255.255.255.0 = 192.168.182.0
192.168.182.48 & 0.0.0.255 = 0.0.0.48
2.套接字
1)什么是套接字?
Socket,电源插座->套接字
一个由系统内核负责维护,通过文件描述符访问的对象, 可用于在同一台机器或不同机器中的进程之间实现通信。
进程表项
文件描述符表
0: 文件描述符标志 | * -> 标准输入文件表项 -> 键盘
1: 文件描述符标志 | * -> 标准输出文件表项 -> 显示器
2: 文件描述符标志 | * -> 标准错误文件表项 -> 显示器
3: 文件描述符标志 | * -> 套接字对象 -> 网卡
应用程序 应用程序
v v
磁盘文件的文件描述符 表示网络的文件描述符
v v
文件对象 套接字对象
v v
文件系统 网络协议栈
v v
磁盘设备 网络设备
套接字也可以被视为是围绕表示网络的文件描述符的一套函数库。调用其中的函数就可以访问网络上的数据,实现不同主机间的通信功能。
2)绑定和连接
套接字就是系统内核内存中的一块数据——逻辑对象
| 绑定(bind)
包含了IP地址和端口号等参数的网络设备——物理对象
互联网
| <-网络地址:192.168.182.0
子网络
| <-主机地址:0.0.0.48
计算机
| <-端口号:80
应用
通过IP地址(网络地址+主机地址)端口号就可以唯一定位互联网上的一个通信引用。
主机A
应用程序
|
逻辑对象(套接字)
| 绑定(bind) 连接(connection)
物理对象(IP地址和端口号)---------物理对象(IP地址和端口号)
| 绑定(bind)
逻辑对象(套接字)
|
应用程序
主机B
3)常用函数
创建套接字
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
成功返回套接字描述符,失败返回-1。
domain - 通信域,即协议族,可取以下值:
PF_LOCAL/PF_UNIX: 本地通信,进程间通信
PF_INET: 互联网通信
PF_PACKET: 底层包通信(嗅探器、端口扫描)
type - 套接字类型,可取以下值:
SOCK_STREAM: 流式套接字,使用TCP协议
SOCK_DGRAM: 数据报式套接字,使用UDP协议
SOCK_RAW: 原始套接字,使用自定义协议
protocol - 特殊协议
对于流式套接字和数据报式套接字,取0
套接字描述符与文件描述符在逻辑层面是一致的,所有关于文件描述符的规则对于套接字描述符也同样成立。同样也通过close函数关闭套接字,即释放内核中的有关资源。
基本地址结构:
struct sockaddr {
sa_family_t sa_family; // 地址族
char sa_data[14]; // 地址值
};
基本地址结构仅用于给函数传参时做强制类型转换。
本地地址结构:
#include <sys/un.h>
struct sockaddr_un {
sa_family_t sun_family; // 地址族
// (AF_LOCAL/AF_UNIX)
char sun_path[]; // 套接字文件路径
};
网络地址结构:
#include <sys/in.h>
struct sockaddr_in {
sa_family_t sin_family; // 地址族(AF_INET)
in_port_t sin_port; // 端口号(网络字节序)
struct in_addr sin_addr; // IP地址
};
struct in_addr {
in_addr_t s_addr; // 网络字节序32位无符号整数形式的
// IP地址
};
typedef uint32_t in_addr_t;
typedef uint16_t in_port_t;
小端字节序的主机A 大端字节序的主机B
short a = 0x1234;
| 0x34 | 0x12 | | 0x34 | 0x12 |
低 高 低 高
a: 0x3412
发送:主机字节序->网络(大端)字节序
接收:网络(大端)字节序->主机字节序
小端字节序的主机A 大端字节序的主机B
0x1234
主:0x34 | 0x12
L H
网:0x12 | 0x34 -> 网:0x12 | 0x34
L H
主:0x12 | 0x34
0x1234
--------------------
小端字节序的主机C
-> 网:0x12 | 0x34
L H
主:0x34 | 0x12
0x1234
将套接字对象和自己的地址结构绑定在一起
int bind(int sockfd, const struct sockaddr* addr,
socklen_t addrlen);
成功返回0,失败返回-1。
sockfd - 套接字描述符
addr - 自己的地址结构
addrlen - 地址结构字节数
addr->sa_family: AF_LOCAL/AF_UNIX
((struct sockaddr_un*)addr)->sun_path: 套接字文件
addr->sa_family: AF_INET
((struct sockaddr_in*)addr)->sin_port/sin_addr:
IP地址和端口号
将套接字对象所代表的物理对象和对方的地址结构连接在一起
int connect(int sockfd, const struct sockaddr* addr,
socklen_t addrlen);
成功返回0,失败返回-1。
sockfd - 套接字描述符
addr - 对方的地址结构
addrlen - 地址结构字节数
通过套接字描述符接收和发送数据的过程完全与通过文件描述符读取和写入数据的过程完全一样。
ssize_t read(int sockfd, void* buf, size_t count);
ssize_t write(int sockfd, const void* buf, size_t count);
字节序转换
通过网络传输多字节整数,需要在发送前转换为网络字节序,在接收后转换为主机字节序。
#include <arpa/inet.h>
uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);
h - host,主机(字节序)
to - 到,把…转换到…
n - network,网络(字节序)
l - long版本,32位无符号整数
s - short版本,16位无符号整数
IP地址转换
(网络字节序32位无符号)整数<=>(点分十进制)字符串
#include <arpa/inet.h>
in_addr_t inet_addr(const char* cp); // 串->数
int inet_aton(const char* cp, struct in_addr* inp);
// 串->数
转换成功返回0,失败返回-1。
char* inet_ntoa(struct in_addr in); // 数->串
转换成功返回字符串指针,失败返回NULL。
基于本地套接字的进程间通信:
服务器:提供业务服务的计算机程序。
客户机:请求业务服务的计算机程序。
服务器 客户机
创建套接字(socket) 创建套接字(socket)
准备地址结构(sockaddr_un) 准备地址结构(sockaddr_un)
绑定地址(bind) 建立连接(connect)
接收请求(read) 发送请求(write)
业务处理(…) 等待处理(…)
发送响应(write) 接收响应(read)
关闭套接字(close) 关闭套接字(close)
代码:locsvr.c、loccli.c
基于网络套接字的进程间通信:
服务器 客户机
创建套接字(socket) 创建套接字(socket)
准备地址结构(sockaddr_in) 准备地址结构(sockaddr_in)
绑定地址(bind) 建立连接(connect)
接收请求(read) 发送请求(write)
业务处理(…) 等待处理(…)
发送响应(write) 接收响应(read)
关闭套接字(close) 关闭套接字(close)
代码:netsvr.c、netcli.c
3.基于并发的TCP客户机/服务器模型
1)TCP协议的基本特征
A.面向连接:参与通信的双发在正式通信之前需要先建立连接,已形成一条虚拟电路,所有的后续通信都在这条虚电路上完成。类似于电话通信业务。正式通话之前要先拨号,拨通了才能讲话。拨号的过程就是一个建立连接的过程。
三路握手
客户机 服务器
发起连接请求 --------SYN(n)--------> 被动侦听
等待应答 <-ACK(n+1)+SYN(m)- 可以接受
反向确认 ------ACK(m+1)------> 连接成功
一旦三路握手完成,客户机和服务器的网络协议栈中就会保存有关连接的信息,此后的通信内容全部基于此连接实现数据传输。通信过程中任何原因导致的连接中断,都无法再继续通信,除非重新建立连接。
B.可靠传输:超时重传。每次发送一个数据包,对方都需要在一个给定的时间窗口内予以应答,如果超过时间没有收到对方的应答,发送方就会重发该数据包,只有重试过足够多的次数依然失败才会最终放弃。
C.保证顺序:发送端为每一个数据包编制序列号,接收端会根据序列号对所接收到的数据包进行重排,避免重复和乱序。
D.流量控制:协议栈底层在从另一端接收数据时,会不断告知对方它能够接收多少字节的数据,即所谓通告窗口。任何时候,这个窗口都反映了接收缓冲区可用空间的大小,从而确保不会因为发送方发送数据过快或过慢导致接收缓冲区出现上溢出或下溢出。
E.流式传输:以字节流形式传输数据,数据包在传输过程中没有记录边界。应用程序需要根据自己的规则来划分出数据包的记录边界。
a)定长记录
b)不定长记录加分隔符
c)定长长度加不定长记录
F.全双工:在给定的连接上,应用程序在任何时候都既可以发送数据也可以接收数据。因此TCP必须跟踪每个方向上数据流的状态信息,如序列号和通告窗口大小等。
2)TCP连接的生命周期
被动打开:通过侦听套接字感知其它主机发起的连接请求。
三路握手:TCP连接的建立过程。
| TCP包头 | TCP包体 |
<20字节>
含有6个标志位:SYN/ACK/FIN/RST/…
发送序列号和应答序列号

数据传输:超时重传、流量控制、面向字节流、全双工
终止连接:
客户机 服务器
主动关闭 ---------FIN(n)-------> 被动关闭
等待应答 <-----ACK(n+1)------ 关闭应答
确定关闭 <--------FIN(m)------- 已经关闭
关闭应答 ------ACK(m+1)-----> 等待应答
3)常用函数
在指定套接字上启动对连接请求的侦听,即将该套接字置为被动模式,因为套接字都缺省为主动模式。
int listen(int sockfd, int backlog);
成功返回0,失败返回-1。
sockfd - 套接字描述符
backlog - 未决连接请求队列的最大长度
在指定的侦听套接字上等待并接受连接请求
int accept(int sockfd, struct sockaddr* addr,
size_t* addrlen);
成功返回连接套接字描述符用于后续通信,失败返回-1。
sockfd - 侦听套接字描述符
addr - 输出连接请求发起者的地址信息
addrlen - 输入输出连接请求发起者地址信息的字节数
该函数由TCP服务器调用,返回排在已决连接队列首部的连接套接字对象的描述符,若已决连接队列为空,该函数会阻塞。
^^^^^^^^
非并发的TCP服务器
创建套接字(socket)
绑定地址(bind)
启动侦听(listen)
等待连接(accept)<–+
接收请求(read)<-+ |
业务处理(…) | |
发送响应(write)–±-+

项目:基于HTTP协议的Web服务器(WWW服务器)
1.HTTP请求和响应
1)HTTP请求
包头<空行>[包体]
方法 路径 协议
| | |
GET / HTTP/1.1

Host: localhost:8000

User-Agent: Mozilla/5.0

Accept: text/html

Connection: keep-alive

[包体]
2)HTTP响应
包头<空行>[包体=页面内容(HTML语言+JavaScript脚本)]
协议 响应码 响应信息(解释响应码的字符串)
| | |
HTTP/1.1 200 OK

Server: Tarena WebServer 1.0

Date: Tue, 14 Jan 2020 01:39:00 GMT

Content-Type: text/html

Connection: keep-alive

[包体=页面内容(HTML语言+JavaScript脚本)]
2.基于多线程的并发服务器模型
创建流式(基于TCP协议)套接字(socket)
设置套接字选项(setsockopt)
准备地址并绑定(bind)
启动侦听(listen)
等待并接受连接请求(accept)<—+ <-连接请求- 浏览器
|
创建子线程持有accept返回 |
的连接套接字与浏览器通信----+
接收并解析来自浏览器的HTTP请求<–+ <-浏览器
找到浏览器请求的资源 |
组织HTTP响应并发送给浏览器--------+ ->浏览器
3.用多个功能模块组成完整的项目
mime模块:文件扩展名与内容类型映射表
mime.h
http模块:实现HTTP协议
http.h、http.c
套接字模块:实现网络通信
socket.h、socket.c
资源模块:查找文件资源
resource.h、resource.c
客户机模块:实现处理客户机业务的子线程
client.h、client.c
信号模块:初始化信号处理
signals.h、signals.c
服务器模块:实现服务器的主线程
server.h、server.c
主模块:实现main函数
main.c
构建脚本:makefile