burpow
第07节-进程是什么

第07节-进程是什么

第 7 节:进程是什么

所属课程:操作系统自学路线:面向网络安全、逆向工程与漏洞分析
所属周次:第 4 周
课程主题:进程模型
本节目标:理解程序和进程的区别,掌握 PID、父子进程、进程状态、PCB、上下文切换等基本概念,并通过 Linux fork 实验观察进程创建行为。


1. 本节课你要学会什么

学完这一节,你应该能回答下面几个问题:

  1. 程序和进程有什么区别?
  2. 为什么同一个程序可以运行多个实例?
  3. PID 是什么?PPID 是什么?
  4. 父进程和子进程是什么关系?
  5. 进程有哪些常见状态?
  6. PCB 是什么?操作系统为什么需要它?
  7. 上下文切换是什么?为什么有成本?
  8. Linux 下 fork 做了什么?
  9. 僵尸进程是什么?
  10. 为什么进程模型对恶意代码分析和逆向很重要?

本节课的主线是:

1
磁盘上的程序 -> 操作系统加载 -> 创建进程 -> 分配资源 -> 调度执行 -> 状态变化 -> 退出回收

前面几节你主要从“程序内部”看代码、内存和调试;从这一节开始,你要从“操作系统外部”看程序如何作为进程被管理。


2. 程序和进程的区别

这是操作系统中最重要的基础概念之一。

2.1 程序是什么

程序通常指磁盘上的可执行文件。

例如 Linux 下:

1
2
3
/bin/ls
/usr/bin/python3
./hello

Windows 下:

1
2
C:\Windows\System32\notepad.exe
C:\Windows\System32\cmd.exe

程序是静态的。

它放在磁盘上,不运行时只是一个文件。


2.2 进程是什么

进程是正在运行的程序实例。

当你执行:

1
./hello

操作系统会:

  1. 读取可执行文件
  2. 创建进程结构
  3. 分配虚拟地址空间
  4. 加载代码和数据
  5. 初始化栈、堆、寄存器
  6. 把它交给 CPU 调度执行

这时它才是一个进程。

所以可以总结为:

1
2
程序 = 磁盘上的静态文件
进程 = 程序运行起来后的动态实例

3. 一个程序可以对应多个进程

同一个可执行文件可以被运行多次。

例如你打开三个终端,分别执行:

1
2
3
sleep 100
sleep 100
sleep 100

它们运行的是同一个程序:

1
/usr/bin/sleep

但操作系统会创建三个不同进程。

每个进程都有自己的:

  • PID
  • 虚拟地址空间
  • 寄存器上下文
  • 打开的文件描述符
  • 当前工作目录
  • 环境变量
  • 权限信息

所以:

1
一个程序文件可以对应多个运行中的进程。

Windows 下同理。

你打开三个记事本:

1
2
notepad.exe 文件只有一个
notepad.exe 进程可以有三个

4. PID 和 PPID

操作系统需要区分不同进程。

所以每个进程都有一个编号,叫:

1
PID = Process ID

父进程 ID 叫:

1
PPID = Parent Process ID

Linux 下可以查看:

1
ps -o pid,ppid,stat,comm

示例输出:

1
2
3
 PID  PPID STAT COMMAND
1234 1200 Ss bash
1300 1234 R+ ps

这表示:

1
2
3
ps 的父进程是 bash
bash 的 PID 是 1234
ps 的 PPID 是 1234

在进程树中,父子关系非常重要。


5. 父进程和子进程

一个进程可以创建另一个进程。

创建者叫父进程。

被创建者叫子进程。

例如你在 shell 中运行:

1
ls

大致过程是:

1
2
3
4
bash 进程创建 ls 进程
ls 运行并输出目录内容
ls 退出
bash 继续等待下一条命令

这里:

1
2
bash 是父进程
ls 是子进程

Linux 下常见进程创建方式是:

1
fork + exec

Windows 下常见方式是:

1
CreateProcess

下一节会专门对比 Linux 和 Windows 的进程创建机制。


6. 进程树

系统中的进程通常形成一棵树。

Linux 下可以使用:

1
pstree

或者:

1
ps -ef --forest

你可能看到类似:

1
2
3
4
5
6
systemd
├─sshd
│ └─bash
│ └─vim
├─cron
└─NetworkManager

这说明:

  • systemd 是很多进程的祖先
  • sshd 创建了 shell
  • shell 又启动了 vim

Windows 下可以用:

  • Process Explorer
  • Process Hacker
  • Task Manager 的详细信息视图

Process Explorer 可以清楚看到进程树。

恶意代码分析时,进程树非常重要。

例如:

1
word.exe -> powershell.exe -> cmd.exe -> unknown.exe

这类链条就很可疑。


7. 进程有哪些资源

一个进程不是只有代码。

操作系统会为进程维护一整套资源。

常见包括:

  • PID
  • 父进程 ID
  • 用户 ID / 权限
  • 虚拟地址空间
  • 代码段
  • 数据段
  • 寄存器状态
  • 打开的文件描述符
  • 当前工作目录
  • 环境变量
  • 信号处理方式
  • 线程列表
  • 调度信息

你可以把进程理解成:

1
程序代码 + 运行时状态 + 操作系统分配的资源

这就是为什么进程比程序复杂得多。


8. PCB 是什么

PCB 全称是:

1
Process Control Block

中文常译为:

1
进程控制块

PCB 是操作系统内核中用来描述和管理进程的数据结构。

它保存了进程的关键信息,例如:

  • PID
  • 进程状态
  • 程序计数器 / 指令位置
  • CPU 寄存器
  • 内存管理信息
  • 打开的文件
  • 调度优先级
  • 父子关系
  • 权限信息
  • 资源使用统计

注意:

PCB 是操作系统内部概念。

普通用户程序不能直接随便访问内核中的 PCB。

但我们可以通过系统工具间接观察进程信息,例如:

1
2
3
4
5
6
ps
top
htop
/proc/<pid>/status
/proc/<pid>/maps
/proc/<pid>/fd

9. Linux 中 PCB 对应什么

Linux 内核中,进程和线程的核心描述结构常和:

1
task_struct

相关。

task_struct 中保存了大量进程或线程状态。

你现在不需要阅读内核源码,只要知道:

1
操作系统必须在内核中维护每个进程的状态,否则无法调度、暂停、恢复、回收进程。

以后学习内核、驱动、Rootkit、内核漏洞时,task_struct 会再次出现。


10. 进程状态

进程在生命周期中会处于不同状态。

常见状态包括:

1
2
3
4
5
6
running
ready
blocked / waiting
stopped
zombie
terminated

不同教材和系统命名略有差异。


10.1 Running:正在运行

进程正在 CPU 上执行。

如果系统只有一个 CPU 核心,同一瞬间真正运行的进程数量有限。

但因为调度切换很快,我们感觉很多程序在“同时运行”。


10.2 Ready:就绪

进程已经准备好运行,但暂时没有拿到 CPU。

它在等待调度器选择它。


10.3 Blocked / Waiting:阻塞或等待

进程暂时不能继续执行,因为它在等待某个事件。

例如:

  • 等待用户输入
  • 等待读文件完成
  • 等待网络数据
  • 等待锁
  • 等待子进程退出

阻塞进程不会一直占用 CPU。


10.4 Stopped:停止

进程被暂停。

例如:

  • 被调试器暂停
  • 收到 SIGSTOP
  • 用户按 Ctrl+Z

10.5 Zombie:僵尸进程

僵尸进程是已经结束执行,但父进程还没有读取它退出状态的进程。

它不再运行,也不占用普通运行资源,但进程表中还保留一条记录。

为什么要保留?

因为父进程可能需要知道:

1
子进程是正常退出还是异常退出?退出码是多少?

父进程调用 waitwaitpid 后,僵尸进程会被回收。


11. Linux ps 中的状态码

Linux 下可以用:

1
ps -o pid,ppid,stat,comm

常见状态码:

状态 含义
R Running 或 Runnable,正在运行或可运行
S Sleeping,可中断睡眠,等待事件
D Uninterruptible sleep,不可中断睡眠,常见于 I/O
T Stopped,停止或被跟踪
Z Zombie,僵尸进程
I Idle,内核空闲线程

可能还会看到附加符号:

符号 含义
s 会话 leader
+ 前台进程组
l 多线程进程
< 高优先级
N 低优先级

这些状态在观察系统行为时很有用。


12. 上下文是什么

进程执行时,CPU 中有很多当前状态。

例如:

  • rip:当前执行位置
  • rsp:当前栈顶
  • 通用寄存器
  • 标志寄存器
  • 内存映射信息
  • 调度相关信息

这些运行状态统称为上下文。

可以粗略理解为:

1
一个进程暂停后,未来要恢复运行所必须保存的信息。

13. 上下文切换是什么

操作系统需要让多个进程共享 CPU。

当 CPU 从进程 A 切换到进程 B 时,需要:

  1. 保存进程 A 的上下文
  2. 更新进程 A 的状态
  3. 选择进程 B
  4. 恢复进程 B 的上下文
  5. 让 CPU 从进程 B 上次暂停的位置继续执行

这叫上下文切换。

1
进程 A -> 保存现场 -> 调度器选择 B -> 恢复现场 -> 进程 B

上下文切换有成本。

因为它需要保存和恢复状态,还可能影响 CPU 缓存、TLB 等硬件结构。

所以线程和进程切换都不是免费的。


14. 为什么一个 CPU 看起来能运行很多程序

如果只有一个 CPU 核心,同一瞬间只能真正执行一个执行流。

但操作系统会快速切换不同进程。

例如:

1
2
3
4
进程 A 运行 5ms
切换到进程 B 运行 5ms
切换到进程 C 运行 5ms
再切回进程 A

因为切换速度很快,人感觉它们在同时运行。

这叫并发。

如果有多个 CPU 核心,多个进程可以真的同时在不同核心上运行。

这叫并行。

区别:

1
2
并发:看起来同时推进
并行:物理上同时执行

15. Linux 下的 fork

Linux 中,创建进程的经典方式是:

1
fork()

fork 会创建当前进程的一个子进程。

子进程从 fork 返回的位置继续执行。

这点非常重要:

1
fork 之后,父子进程都会继续执行 fork 后面的代码。

区别在于返回值不同。


16. fork 的返回值

fork 的返回值用于区分父子进程。

返回值 含义
小于 0 创建失败
等于 0 当前在子进程中
大于 0 当前在父进程中,返回值是子进程 PID

示例:

1
2
3
4
5
6
7
8
9
pid_t pid = fork();

if (pid == 0) {
// 子进程
} else if (pid > 0) {
// 父进程
} else {
// fork 失败
}

这是 Linux 进程编程最基本的模式。


17. 第一个 fork 程序

创建 fork_demo.c

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>
#include <unistd.h>

int main() {
printf("before fork\n");

fork();

printf("after fork pid=%d ppid=%d\n", getpid(), getppid());
return 0;
}

编译:

1
gcc fork_demo.c -o fork_demo

运行:

1
./fork_demo

你可能看到:

1
2
3
before fork
after fork pid=1234 ppid=1200
after fork pid=1235 ppid=1234

为什么 after fork 打印两次?

因为 fork 之后,有两个进程继续执行后面的代码:

  • 父进程
  • 子进程

18. fork 后父子进程内存是否一样

从程序视角看,fork 后子进程像是父进程的一份拷贝。

它继承了很多东西:

  • 代码
  • 数据
  • 打开的文件描述符
  • 当前工作目录
  • 环境变量

但父子进程是两个独立进程。

它们有不同 PID。

现代操作系统通常使用 Copy-on-Write。

也就是:

1
2
fork 后先共享物理页面
只有当某一方尝试写入时,才真正复制页面

这样可以提高效率。

入门阶段先记住:

1
fork 后父子进程看起来内存内容一样,但之后各自修改互不影响。

19. fork 后变量变化实验

创建 fork_var_demo.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
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>

int global_var = 100;

int main() {
int local_var = 200;

pid_t pid = fork();

if (pid == 0) {
global_var += 1;
local_var += 1;
printf("child: global=%d local=%d pid=%d\n", global_var, local_var, getpid());
} else if (pid > 0) {
wait(NULL);
printf("parent: global=%d local=%d pid=%d\n", global_var, local_var, getpid());
} else {
perror("fork");
return 1;
}

return 0;
}

编译运行:

1
2
gcc fork_var_demo.c -o fork_var_demo
./fork_var_demo

你可能看到:

1
2
child: global=101 local=201 pid=1235
parent: global=100 local=200 pid=1234

这说明:

1
子进程修改变量,不会影响父进程中的变量。

20. wait 是什么

父进程通常需要等待子进程结束。

Linux 中可以用:

1
wait(NULL);

或者:

1
waitpid(pid, &status, 0);

作用:

  • 等待子进程退出
  • 获取子进程退出状态
  • 回收子进程资源
  • 避免僵尸进程

如果父进程不调用 wait,子进程结束后可能暂时变成僵尸进程。


21. 僵尸进程演示

创建 zombie_demo.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

int main() {
pid_t pid = fork();

if (pid == 0) {
printf("child exit, pid=%d\n", getpid());
exit(0);
} else if (pid > 0) {
printf("parent sleep, pid=%d child=%d\n", getpid(), pid);
sleep(30);
} else {
perror("fork");
return 1;
}

return 0;
}

编译运行:

1
2
gcc zombie_demo.c -o zombie_demo
./zombie_demo

另开终端查看:

1
ps -o pid,ppid,stat,comm | grep zombie_demo

你可能看到子进程状态是:

1
Z

这就是僵尸进程。

注意:

这个实验是安全的,父进程睡眠结束后僵尸进程会被回收。


22. 孤儿进程是什么

如果父进程先退出,子进程还在运行,这个子进程就变成孤儿进程。

孤儿进程会被系统中的某个进程接管。

在很多 Linux 系统上,通常由 systemd 或 init 类进程接管。

孤儿进程本身不一定是问题。

但在恶意代码分析中,如果一个进程故意脱离父进程、隐藏运行,就值得关注。


23. 进程和线程的初步区别

本节主要讲进程,后面会专门讲线程。

先简单区分:

对比 进程 线程
资源拥有 拥有独立地址空间和资源 属于进程,和同进程线程共享资源
隔离性 较强 较弱
创建开销 较大 较小
通信 进程间通信较复杂 共享内存更方便
崩溃影响 一个进程崩溃通常不直接毁掉另一个进程 一个线程崩溃可能导致整个进程崩溃

先记住:

1
2
进程是资源分配的基本单位
线程是 CPU 调度执行的重要单位

不同教材说法可能略有差异,但这个理解对入门很有帮助。


24. Windows 中的进程初步

Windows 中也有进程和线程。

创建进程常用:

1
CreateProcess

Windows 进程也有:

  • PID
  • 父进程
  • 虚拟地址空间
  • 句柄表
  • Token
  • 线程
  • DLL 模块
  • 环境变量

可以用 Process Explorer 查看:

  • 进程树
  • PID
  • PPID
  • 命令行
  • 当前用户
  • 线程数量
  • 打开的句柄
  • 加载的 DLL

Windows 安全分析中,进程树非常重要。

例如:

1
2
3
explorer.exe -> unknown.exe
winword.exe -> powershell.exe
chrome.exe -> cmd.exe

这些父子关系可能体现用户行为,也可能体现攻击链。


25. 进程行为和恶意代码分析

恶意代码经常会创建新进程。

常见目的包括:

  • 启动系统工具执行命令
  • 创建子进程隐藏行为
  • 注入其他进程
  • 启动持久化组件
  • 逃避简单检测
  • 执行下载的 payload

常见可疑进程链:

1
2
3
4
5
Office 文档 -> powershell.exe
浏览器 -> cmd.exe
脚本解释器 -> 可疑 exe
压缩包程序 -> 临时目录 exe
服务进程 -> 异常子进程

这不是说这些链条一定恶意,而是需要进一步分析。

工具:

  • Windows Process Explorer
  • Windows Process Monitor
  • Sysmon
  • Linux ps / pstree / auditd

26. 进程行为和逆向工程

逆向一个程序时,你不只看代码,还要看它运行后创建了什么进程。

例如:

  • 是否调用 fork
  • 是否调用 execve
  • 是否调用 system
  • 是否调用 CreateProcessW
  • 是否启动 shell
  • 是否把参数传给子进程
  • 是否等待子进程结束

如果一个程序表面上什么都不做,但运行后启动了另一个程序,那么真正逻辑可能在子进程中。

所以动态分析时要观察进程树。


27. 进程行为和漏洞分析

进程模型也和漏洞分析有关。

例如:

  • 一个网络服务可能为每个连接创建子进程
  • 一个崩溃只影响当前 worker 进程
  • 父进程可能负责重启子进程
  • 子进程权限可能比父进程低
  • setuid 程序创建子进程时可能引入风险
  • 子进程继承文件描述符可能造成信息泄露

真实漏洞分析中,你经常要搞清楚:

1
2
3
4
5
漏洞发生在哪个进程?
这个进程是什么权限?
它是谁启动的?
它继承了哪些资源?
崩溃后是否会自动重启?

28. Linux 实验:观察进程和 PID

执行:

1
ps -o pid,ppid,stat,comm

再执行:

1
sleep 100

另开一个终端:

1
ps -o pid,ppid,stat,comm | grep sleep

观察:

  • sleep 的 PID 是什么
  • 它的 PPID 是谁
  • 它的状态是什么

也可以用:

1
pstree -p

观察进程树。


29. Linux 实验:fork 创建子进程

创建 fork_demo.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>
#include <unistd.h>

int main() {
printf("before fork pid=%d\n", getpid());

pid_t pid = fork();

if (pid == 0) {
printf("child: pid=%d ppid=%d\n", getpid(), getppid());
} else if (pid > 0) {
printf("parent: pid=%d child=%d\n", getpid(), pid);
} else {
perror("fork");
return 1;
}

return 0;
}

编译运行:

1
2
gcc fork_demo.c -o fork_demo
./fork_demo

观察:

  • before fork 打印几次?
  • parent 打印几次?
  • child 打印几次?
  • 父进程中 fork 返回值是什么?
  • 子进程中 fork 返回值是什么?

30. Linux 实验:用 strace 观察 fork

执行:

1
strace ./fork_demo

你可能看到类似:

1
clone(...)

在现代 Linux 中,fork 底层可能通过 clone 系统调用实现。

这说明:

1
C 库中的 fork 最终需要请求内核创建新执行实体。

如果输出太多,可以尝试:

1
strace -f ./fork_demo

-f 表示跟踪子进程。

观察父子进程输出对应的系统调用。


31. Windows 实验:Process Explorer 观察进程树

在 Windows 下:

  1. 打开 Process Explorer
  2. 启动 notepad.exe
  3. 查看它的 PID
  4. 查看父进程
  5. 查看线程数量
  6. 查看加载的 DLL
  7. 关闭 notepad,观察进程消失

再打开 cmd.exe,执行:

1
notepad.exe

观察:

  • notepad 的父进程是否是 cmd
  • 如果从 Explorer 双击启动,父进程是否不同

这能帮助你理解:

1
同一个程序由不同父进程启动,进程树会不同。

32. 本节重点总结

你需要记住这些核心结论:

  1. 程序是磁盘上的静态文件,进程是运行中的程序实例。
  2. 同一个程序可以对应多个进程。
  3. PID 是进程 ID,PPID 是父进程 ID。
  4. 进程之间可以形成父子关系和进程树。
  5. 操作系统通过 PCB 管理进程状态和资源。
  6. 进程常见状态包括运行、就绪、阻塞、停止、僵尸等。
  7. 上下文切换是操作系统保存一个进程状态并恢复另一个进程状态的过程。
  8. Linux 下 fork 会创建子进程,父子进程从 fork 后继续执行。
  9. fork 在父进程和子进程中返回值不同。
  10. 进程树和进程行为是恶意代码分析、逆向分析和漏洞分析的重要线索。

33. 本节课后作业

作业 1:观察系统进程

执行:

1
2
ps -o pid,ppid,stat,comm
pstree -p

提交内容:

1
2
3
4
5
1. 当前 shell 的 PID
2. 当前 shell 的 PPID
3. 至少 5 个进程及其状态
4. 你观察到的一条父子进程关系
5. R/S/Z/T 状态分别大致代表什么

作业 2:fork 程序实验

写并运行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>
#include <unistd.h>

int main() {
printf("before fork pid=%d\n", getpid());

pid_t pid = fork();

if (pid == 0) {
printf("child: pid=%d ppid=%d\n", getpid(), getppid());
} else if (pid > 0) {
printf("parent: pid=%d child=%d\n", getpid(), pid);
} else {
perror("fork");
return 1;
}

return 0;
}

提交内容:

1
2
3
4
5
6
1. 源代码
2. 运行输出
3. 父进程 PID
4. 子进程 PID
5. fork 在父进程中的返回值
6. fork 在子进程中的返回值

作业 3:fork 后变量是否共享

运行 fork_var_demo.c

提交内容:

1
2
3
4
1. 子进程修改后的 global/local 值
2. 父进程看到的 global/local 值
3. 子进程修改变量是否影响父进程
4. 你对 Copy-on-Write 的初步理解

作业 4:观察僵尸进程

运行 zombie_demo.c

在父进程 sleep 期间执行:

1
ps -o pid,ppid,stat,comm | grep zombie_demo

提交内容:

1
2
3
4
5
1. 父进程 PID
2. 子进程 PID
3. 子进程状态是否出现 Z
4. 僵尸进程为什么会出现
5. wait 为什么能回收子进程

作业 5:Windows 进程树观察

如果你有 Windows 环境:

  1. 用 Process Explorer 打开进程树
  2. 从 cmd 启动 notepad
  3. 从资源管理器双击启动 notepad
  4. 对比父进程

提交内容:

1
2
3
4
1. 两种启动方式下 notepad 的父进程
2. notepad 的 PID
3. notepad 加载的几个 DLL
4. 为什么进程树对安全分析有用

34. 自测题

题 1

程序和进程有什么区别?

题 2

为什么同一个程序可以运行多个进程?

题 3

PID 和 PPID 分别是什么?

题 4

PCB 的作用是什么?

题 5

什么是上下文切换?

题 6

fork 后为什么父子进程都会继续执行后面的代码?

题 7

fork 在父进程和子进程中的返回值有什么区别?

题 8

什么是僵尸进程?

题 9

为什么进程树对恶意代码分析重要?


35. 自测题参考答案

答 1

程序是磁盘上的静态可执行文件;进程是程序被操作系统加载后正在运行的实例,包含代码、内存、寄存器状态、打开文件、权限等运行时资源。

答 2

因为可执行文件只是模板。每次运行它,操作系统都可以创建一个新的进程实例,每个实例有自己的 PID、地址空间和运行状态。

答 3

PID 是进程 ID,用来唯一标识一个进程;PPID 是父进程 ID,用来表示该进程由哪个父进程创建。

答 4

PCB 是操作系统内核用于描述和管理进程的数据结构,保存进程状态、寄存器、内存管理信息、打开文件、调度信息、权限等。

答 5

上下文切换是 CPU 从一个进程切换到另一个进程时,操作系统保存当前进程运行状态并恢复另一个进程运行状态的过程。

答 6

因为 fork 创建子进程后,子进程从 fork 返回的位置开始继续执行,父进程也继续执行,所以父子进程都会执行 fork 后面的代码。

答 7

fork 在子进程中返回 0;在父进程中返回子进程的 PID;如果失败返回负数。

答 8

僵尸进程是已经结束执行,但父进程还没有读取其退出状态的进程。它保留在进程表中,等待父进程调用 waitwaitpid 回收。

答 9

因为恶意代码常通过创建子进程、启动系统工具、注入其他进程等方式执行行为。进程树能帮助分析程序的启动链、父子关系和可疑行为路径。


36. 下一节预告

下一节课会讲:

1
进程创建的 Linux / Windows 对照

你会学习:

  • Linux 的 forkexecvewait
  • Windows 的 CreateProcess
  • 为什么 Windows 没有传统 fork
  • 父子进程继承了什么
  • 恶意代码为什么经常创建子进程
  • 如何用 strace 和 Process Monitor 观察进程创建
隐藏
换装
本文作者:burpow
本文链接:https://youthfulnesszxx.github.io/2026/05/28/第07节-进程是什么/
版权声明:本文采用 CC BY-NC-SA 3.0 CN 协议进行许可