进程是进程实体的运行过程,是系统进行资源分配和调度的一个单位。
概念 | 说明 |
---|---|
程序 | 是指静态的存放在磁盘里、的可执行的文件,实际上为一系列的指令集合 |
进程 | 是指动态的、程序的一次执行过程 |
这些信息都将被保存在一个数据结构PCB(Process Control Block)中,即进程控制块。
操纵系统需要对各个并发运行的进程进行管理,但凡管理时所需要的信息都将被放入在PCB中。
一个进程实体(进程映像)由PCB、程序段、数据段组成,进程实体反应了进程在某一时刻的状态。
进程是动态的,进程实体(进程映像)是静态的。进程是进程实体的运行过程,是系统进行资源分配和调度的一个独立单位。
注:PCB是为操作系统而准备的,而程序段和数据段是给进程使用的,PCB是进程存在的唯一标志。
程序是静态的,进程是动态的,相比于程序进程拥有以下特征:
创建态:
进程正在被创建时其状态为创建态,在该阶段操作系统会为进程分配资源、初始化PCB。
就绪态:
当进程创建完成后便进入就绪态,处于就绪态的进程已经具备运行条件,但是由于没有空闲的CPU暂时不能运行。
运行态:
如果一个进程此时在CPU上运行就处于运行态,CPU会执行该进程对应的程序(执行指令序列)。
阻塞态:
在进程运行的过程中,可能会请求等待某个事件的发生。(如等待某种系统资源的分配、等待其他进程的响应等)
在等待过程中进程无法继续往下执行,OS会将进程下CPU,使其进入阻塞状态。
终止态:
一个进程可以执行exit
系统调用,请求操作系统终止该进程。
此时该进程会进入终止态,操作系统会让该进程下CPU并回收内存空间等资源包括进程的PCB。
在进程PCB中会有一个state变量来表示进程的当前状态,如创建态1、就绪态2、运行态3、阻塞态4、终止态5。
操作系统会将各个进程的PCB组织起来,实现对相同状态的各个进程进行管理。
链接方式(队列):
操作系统会管理一系列的队列,每个队列都会指向相应状态下的PCB:
索引方式(索引表):
操作系统会给各种状态的进程建立相应的索引表,每个索引表的表项会指向相应的PCB:
进程控制的主要功能是对系统中的所有进程实施有效的管理,具有创建新进程、撤销已有进程、实现进程状态转换等功能。
进程控制通过原语来实现。
而原语是一种特殊的程序,其执行具有原子性。即该程序的执行(进程状态转换)必须一次完成,不可被中断。
如果原语的执行(进程的状态转换)不能一次完成,就可能导致OS中某些关键数据结构信息不统一影响管理工作。
可以使用关中断指令和开中断指令这连个特权指令,来实现原语的原子性。
创建原语:
撤销原语:
阻塞与唤醒原语:
切换原语:
进程是分配系统资源的单位(包括内存地址),因此各进程拥有的内存地址空间相互独立。
为了保证进程安全,1个进程不能直接访问另1个进程的地址空间,为了保证进程通信间的安全性,操作系统提供了一些方法。
基于数据结构的共享:
基于数据结构,共享空间里只能存放一个长度为10的数组。这种共享方式速度慢、限制多是一种低级的通信方式。
基于存储区的共享:
基于存储区,在内存中的共享存储区数据的形式、存放的位置都由进程控制而不是操作系统。共享速度更快是一种高级的通信方式。
进程间的数据交换以格式化的消息Message为单位,进程通过操作系统提供的发送消息/接收消息两个原语进行数据交换。
直接通信方式:
直接通信方式,是指将格式化消息直接挂到接收进程的消息缓冲队列上,如图:
间接通信方式:
间接通信方式,是指需要先将格式化的消息发送到中间实体(信箱)中,因此也被称为信箱通信方式(电子邮件系统)。
管道是指用于连接读写进程的一个共享文件(pipe文件),本质是在内存中开辟的一个大小固定的缓冲区。
write()
系统调用将被阻塞,等待读进程将数据全部取走。read()
系统调用将被阻塞。传统的进程只能串性地执行一系列程序,为此引入了线程的概念来增加并发度。
传统的进程是程序执行流的最小单位,引入线程后线程成了程序执行流的最小单位,是一个基本的CPU执行单元。
线程引入带来的变化:
线程基本属性:
线程切换中的属性:
用户级线程:
ULT用户级线程User-Level Thread,早期的操作系统只支持进程、不支持线程,线程是由线程库实现的。
很多编程语言提供了强大的线程库,可以实现线程的创建、销毁、调度等功能。
优点 | 缺点 |
---|---|
1.线程切换在用户空间就能够完成,不需要切换到核心态。 | 1.某个用户级线程阻塞后,整个进程都会阻塞并发度不高。 |
2.线程管理的系统开销小、效率高。 | 2.多个线程不可在多核处理机上并行运行。 |
内核级线程:
KLT内核级线程Kernel-Level Thread,内核支持的线程。
优点 | 缺点 |
---|---|
1.当一个线程被阻塞后,其他线程能继续并发能力强。 | 1.一个用户进程占用多个内核级线程,线程切换由操作系统完成。 |
2.多线程可在多核处理机上并行执行。 | 2.线程切换需要切换到核心态,因此线程管理的成本高、开销大。 |
注:操作系统只看得见内核级线程,因此只有内核级线程才是处理机分配的单位。
在支持内核级线程的操作系统中再引入线程库,实现用户级线程与内核级线程结合使用。
一个用户级线程映射到一个内核级线程,每个用户进程与用户级线程同等数量的内核级线程。
优点 | 缺点 |
---|---|
1.当一个线程被阻塞后,其他线程能够继续并发能力强。 | 1.用户进程会占用多个内核级线程,线程切换由操作系统内核完成 |
2.多线程可在多核处理机上并行执行。 | 2.线程切换需要切换到核心态,因此线程管理的成本高、开销大。 |
多个用户级线程映射到一个内核级线程,且1个进程只被分配1个内核级线程。
优点 | 缺点 |
---|---|
1.线程切换在用户空间即可完成,不需要切换到核心态。 | 1.某个用户级线程阻塞后,整个进程都会阻塞,并发度不高。 |
2.线程管理的系统开销小、效率高。 | 2.多个线程不可在多核处理机上并行运行。 |
多对多模型:n个用户及线程映射到m个内核级线程(n>=m),每个用户进程对应m个内核级线程。
当有一堆任务需要处理时,由于资源有限无法全部同时处理。这时需要某种规则来决定处理这些任务的顺序,这就是调度的问题。
高级调度:作业调度
作业:是指一项具体的任务。用户向系统提交一个作业:用户让操作系统启动一个程序(处理一个具体的任务)。
中级调度:内存调度
注:单个进程可能会被多次调入、调出内存,因此中级调度发生的频率比高级调度更高。
低级调度:进程调度/处理机调度
暂时调回外存等待的进程状态称为挂起状态(挂起态,suspend)
将挂起状态进一步细分可分为就绪挂起、阻塞挂起两种状态,则将五模型进一步补充为七模型:
进程调度,就是按照某种算法从就绪队列中选择一个进程为其分配处理机。
抢占式调度方式:
抢占式调度方式,又称为剥夺调度方式。
当一个进程正在处理机上执行时,如果有一个更重要 or 更紧迫的进程需要使用处理机,则立即暂停正在执行的进程,将处理机分配给更重要紧迫的那个进程。
非抢占式调度方式:
非抢占式调度方式,又称为非剥夺调度方式。
只允许进程主动放弃处理机,在运行过程中即便有更紧迫的任务到达,当前进程依然会继续使用处理机,直到该进程终止 or 主动要求进入阻塞状态。
进程切换的过程完成的任务:
注:进程切换是有代价的,因此如果过于频繁的进行进程调度&切换,必然会使整个系统的效率降低。
CPU利用率:指的是CPU忙碌的时间占总时间的比例(通常考查多道程序并发执行的情况)
系统吞吐量:单位时间内完成作业的数量,
周转时间:指从作业被提交给系统开始到作业完成为止,这段时间的间隔
包括作业在外存后备队列上等待作业调度(高级调度)的时间、
进程在就绪队列上等待进程调度(低级调度)的时间、
进程在CPU上执行的时间、
进程等待IO操作完成的时间
等待时间:指进程 or 作业处于等待处理机状态的时间之和,等待时间越长用户满意度越低。
响应时间:指从用户提交请求到首次产生响应所用的时间。
FCFS,First Come First Serve先来先服务算法
SJF,Shortest Job First短作业优先算法(非抢占式)
SRTN,Shortest Remaining Time Next最短剩余时间优先算法(抢占式)
HRRN,Hightest Response Ratio Next高响应比优先算法
批处理系统的调度算法主要关心对用户的公平性、平均周转时间、平均等待时间等评价系统整体性能的指标,
不关心响应时间、不区分任务的紧急程度,因此对于用户来说交互性十分的糟糕(早期的批处理系统)。
RR,Round-Robin时间片轮转
优先级调度算法:
设置多级就绪队列,各级队列优先级从高到低,时间片从小到大
新进程到达时先进入第1级队列,按照FCFS原则排队等待被分配时间片。
若用完时间片进程还未结束,则进程进入下一级队列队尾。
如果此时已经在最下级的队列,则重新放回最下级队列队尾。
只有第k级队列为空时,才会为k+1级队头的进程分配时间片。
被抢占处理机的进程重新放回原队列的队尾,详细讲解。
时间段内只允许一个进程使用的资源称为临界资源,进程互斥是指当一个进程访问临界资源时,另一个进程必须等待其结束。
临界资源的访问逻辑:
临界区是进程中访问临界资源的代码段
进入区和退出区是负责实现互斥的代码段
进程互斥遵循的4个原则:
算法思想:两个进程在访问完邻接资源后会把使用临界区的权限转交给另一个进程,每个进程进入临界区的权限只能被另一个进程赋予。
单标志法缺点:违反了空闲让进的原则。
算法思想:
flag[]
,数组中的各个元素用来标记各进程进入临界区的意愿。flag[0]=true;flag[i]
设置为true,之后开始访问临界区。双标志先检查法的缺点:在进程并发时有可能导致临界资源的多进程访问,违反了忙则等待原则。
算法思想:
flag[]
,数组中的各个元素用来标记各进程进入临界区的意愿。flag[0]=true;flag[i]
设置为true双标志后检查法的缺点:在进程并发时有可能导致各进程都长期无法访问临界资源的饥饿现象,违反了空闲让进与有限等待原则。
Peterson算法结合了双标志与单标志的思想,如果双方都想进入临界区,则可以让某个进程进行谦让。
Peterson算法利用软件方法解决了进程互斥问题,遵循了空闲让进、忙着等待、有限等待原则,但是仍然未遵循让权等待原则。
中断屏蔽算法是利用开/关中断指令实现,与原语的实现思想相同,在某进程开始开始访问临界资源到结束都不允许被中断。
TSL指令是利用硬件实现的,执行过程不允许被中断。
TSL指令将上锁与检查操作,用硬件的方式变成了一气呵成的原子操作。
Swap/XCHG指令利用硬件实现执行的过程不允许被中断,只能一气呵成。
逻辑上看Swap与TSL并无太大区别,
用户进程可以通过使用操作系统提供的一对原语来对信号量进行操作,从而很方便的实现了进程互斥、进程同步。
用一个整数型变量作为信号量,用来表示系统中某种资源的数量。对信号量的操作只有三种:初始化、P操作、V操作
int S = 1; //初始化整型信号量S,表示当前系统中可用资源(打印机)的数量
void wait(int S) { //wait原语相当于进入区
while (S <= 0);//如果资源不够则一直循环等待
S = S - 1;//如果资源足够就占用一个资源
}
void signal(int S) { //signal原语相当于退出区
S = S + 1;//使用完资源后,在退出区释放资源
}
整型信号量有一个很大的缺陷:
如果一个进程暂时无法进入临界区(资源不够),该进程会一直占用处理机循环检查导致忙等,可以采用记录型信号量解决。
记录型信号量即用记录型数据结构表示信号量。
/*记录型信号量定义*/
typedef struct {
int value;//剩余资源数
struct process *L;//等待队列
} semaphore;
void wait(semaphore S) {
S.value--;
if (S.value < 0) {
block(S.L);
}
}
void signal(semaphore S) {
s.value++;
if (S.value <= 0) {
wakeup(S.L);
}
}
进程互斥的软件与硬件实现都没有解决让权等待的问题,用信号量机制中的阻塞与唤醒机制可以解决:
信号量机制实现进程互斥:
/*信号量机制实现互斥*/
semaphore mutex = 1;
P1() {
...
P(mutex);//使用临界资源前需要加锁
临界区代码段...
V(mutex);//使用临界资源后需要解锁
...
}
P2() {
...
P(mutex);
临界区代码段...
V(mutex);
...
}
信号量机制实现进程同步(难点):
进程同步:是指要让各个并发的进程按照要求有序的推进。
V(S)
P(S)
/*信号量机制实现同步*/
semaphore S = 0;
P1() {
代码1;
代码2;
V(S);//释放资源
代码3;
}
P2() {
P(S);
代码4;
代码5;
代码6;
}
要求代码按照如下前驱图所示的顺序来执行,每1对前驱关系都是1个进程同步问题(多级同步问题)
PV操作问题分析步骤:
系统中有一组生产者进程和一组消费者进程,
生产者进程每次生产一个产品放入缓冲区,消费者进程每次从缓冲区中取出一个产品并使用。共享一个初始为空、大小为n的缓冲区。
/*生产者消费者问题核心代码*/
void producer() {
while(1) {
produce a good;
P(empty);//消耗一个空闲缓冲区
pull this good into buffer;
V(full);//增加一个产品数量
}
}
void consumer() {
while(1) {
P(full);//申请消耗一个产品
take this good out from buffer;
V(empty);//增加一个空闲缓冲区
consume a good;
}
}
semaphore mutex = 1;//互斥信号量
semaphore empty = n;//同步信号量
semaphore full = 0;//同步信号量
void producer() {
while(1) {
produce a good;
P(empty);
P(mutex);
pull this good into buffer;
V(mutex);
V(full);
}
}
void consumer() {
while(1) {
P(full);
P(mutex);
take this good out from buffer;
V(mutex);
V(empty);
consume a good;
}
}
有的时候是生产者进程需要等待消费者进程,有的时候是消费者进程需要等待生产者进程,需要设置两对不同的信号量。
注:实现互斥的P操作一定要在实现同步的P操作之后,否则会出现死锁的情况。
/*多生产者多消费者问题核心代码*/
void father() {
prepare a apple;
P(plate);
put this apple into the plate;
V(apple);
}
void mother() {
prepare a orange;
P(plate);
put this orange into the plate;
V(orange);
}
void daughter() {
P(apple);
take this orange from the plate;
V(plate);
eat this apple;
}
void son() {
P(orange);
take this apple from the plate;
V(plate);
eat this orange;
}
semaphore mutex = 1;//缓冲区互斥
semaphore apple = 0;//盘子中苹果的数量
semaphore orange = 0;//盘子中橘子的数量
semaphore plate = 1;//盘子中还可以方多少个水果
void father() {
prepare a apple;
P(plate);
P(mutex);
put this apple into the plate;
V(mutex);
V(apple);
}
void mother() {
prepare a orange;
P(plate);
P(mutex);
put this orange into the plate;
V(mutex);
V(orange);
}
void daughter() {
P(apple);
P(mutex);
take this orange from the plate;
V(mutex);
V(plate);
eat this apple;
}
void son() {
P(orange);
P(mutex);
take this apple from the plate;
V(mutex);
V(plate);
eat this orange;
}
可省去互斥mutex信号量的PV操作,
原因是缓冲区的大小为1,在任意时刻apple、orange、plate同步信号量中最多只有1个为1,在任意时刻最多只有一个进程的P操作。
semaphore mutex = 1;//缓冲区互斥
semaphore apple = 0;//盘子中苹果的数量
semaphore orange = 0;//盘子中橘子的数量
semaphore plate = 1;//盘子中还可以方多少个水果
void father() {
prepare a apple;
P(plate);
put this apple into the plate;
V(apple);
}
void mother() {
prepare a orange;
P(plate);
put this orange into the plate;
V(orange);
}
void daughter() {
P(apple);
take this orange from the plate;
V(plate);
eat this apple;
}
void son() {
P(orange);
take this apple from the plate;
V(plate);
eat this orange;
}
semaphore offer1 = 0;
semaphore offer2 = 0;
semaphore offer3 = 0;
semaphore finish = 0;
int i = 0;//用于实现抽烟者轮流抽烟
void provider() {
while(1) {
if (i == 0) {
Put combination 1 into the buffer;
V(offer1);
} else if (i == 1) {
Put combination 2 into the buffer;
V(offer2);
} else if (i == 2) {
Put combination 3 into the buffer;
V(offer3);
}
i = (i + 1) % 3;
P(finish);
}
}
void smoker1() {
while(1) {
P(offer1);
take combination1 from the buffer;
V(finish);
}
}
void smoker2() {
while(1) {
P(offer1);
take combination1 from the buffer;
V(finish);
}
}
void smoker3() {
while(1) {
P(offer1);
take combination1 from the buffer;
V(finish);
}
}
有读者写者两组并发进程共享一个文件,
当两个或两个以上的读进程同时访问共享数据时,不会产生副作用。
但若某个写进程和其他进程(读进程or写进程)同时访问共享数据时,则可能导致数据不一致的错误。因此要求:
1.允许多个读者可以同时对文件执行读操作
2.只允许一个写者往文件中写信息
3.任一写者在完成写操作之前不允许其他读者或写者工作
4.写者执行写操作之前,应该让已有的读者和写者全部都退出
两类进程:写进程、读进程
互斥关系:写进程-写进程、写进程-读进程、读进程与读进程之前不存在互斥关系
/*1.读者写者互斥访问文件*/
semaphore rw = 1;//用于实现对共享文件的互斥访问
void writer() {
while(1) {
P(rw);//写文件前加锁
write data to the file;
V(rw);//写文件后解锁
}
}
void reader() {
while(1) {
P(rw);
read data from the file;
V(rw);
}
}
/*读写与读写之间不可同时访问文件*/
/*1.读者写者互斥访问文件*/
/*2.修改读者与读者之间可同时访问文件*/
semaphore rw = 1;
count = 0;//用于记录当前有几个读进程在访问文件
void writer() {
while(1) {
P(rw);//写文件前加锁
write data to the file;
V(rw);//写文件后解锁
}
}
void reader() {
while(1) {
if (count == 0) P(rw);
count++;
read data from the file;
count--;
if (count == 0) V(rw);
}
}
/*如果两个读进程并发执行,则count==0时两个进程也许能同时通过if语句,从而都会执行P(rw)导致第二个读进程阻塞*/
/*问题源头:count变量的检查与赋值无法一气呵成,中间有可能有并发进程插入*/
/*1.读者写者互斥访问文件*/
/*2.修改读者与读者之间可同时访问文件*/
/*3.设置互斥信号量mutext用于保证对count的互斥访问*/
semaphore rw = 1;//用于实现对共享文件的互斥访问
count = 0;//用于记录当前有几个读进程在访问文件
semaphore mutex = 1;//新增mutex互斥信号量,用于保证对count的互斥访问
void writer() {
while(1) {
P(rw);//写文件前加锁
write data to the file;
V(rw);//写文件后解锁
}
}
void reader() {
while(1) {
P(mutex);
if (count == 0) P(rw);
count++;
V(mutex);
read data from the file;
P(mutex);
count--;
if (count == 0) V(rw);
V(mutex);
}
}
/*只要有读进程还在继续,写进程就需要一直阻塞可能饿死*/
/*1.读者写者互斥访问文件*/
/*2.修改读者与读者之间可同时访问文件*/
/*3.设置互斥信号量mutext用于保证对count的互斥访问,解决第二个读进程阻塞的问题*/
/*4.设置互斥信号量w用于实现写进程优先,解决写进程饥饿的问题*/
semaphore rw = 1;//用于实现对共享文件的互斥访问
count = 0;//用于记录当前有几个读进程在访问文件
semaphore mutex = 1;//用于保证对count的互斥访问
semaphore w = 1;//用于实现写优先
void writer() {
while(1) {
P(w);
P(rw);//写文件前加锁
write data to the file;
V(rw);//写文件后解锁
V(w);
}
}
void reader() {
while(1) {
P(w);
P(mutex);
if (count == 0) P(rw);
count++;
V(mutex);
V(w);
read data from the file;
P(mutex);
count--;
if (count == 0) V(rw);
V(mutex);
}
}
读者与写者问题的核心思想在于:
一张圆桌上坐着5名哲学家,每两个哲学家之间的桌上摆着一根筷子,桌子的中间是一碗米饭。
哲学家在思考时并不影响其他人,只有当哲学家饥饿时才会试图拿起左右两侧的筷子,如果筷子已在其他人的手上则需要等待。
饥饿的哲学家只有同时拿起两根筷子时才能开始进餐,进餐完毕后继续思考。
哲学家进餐问题中只有互斥关系,但与之前遇到的问题不同的是,每个则学家需要同时持有两个临界资源,因此有死锁的隐患。
如何避免临界资源分配不当造成死锁的问题,是哲学家进餐问题的精髓。
信号量设置:
chopstick[5]={1,1,1,1,1}
用于实现对5根筷子的互斥访问。i
左边的筷子编号为i
,右边的筷子编号为(i+1)%5
semaphore chopstick[5] = {1,1,1,1,1};
pi() {//i号哲学家的进程
while(1) {
P(chopstick[i]);
P(chopstick[(i+1)%5]);
Have a meal;
V(chopstick[i]);
V(chopstick[(i+1)%5]);
Ponder questions;
}
}
/*存在问题当如果5个哲学家并发的拿起了自己左手边的筷子,则所有哲学家都会循环等待右边的人放下筷子(阻塞)造成死锁*/
/*1.添加限制条件最多只允许4个哲学家进餐,以解决死锁问题*/
/*2.添加限制条件:
要求奇数号哲学家先拿起左边的筷子,然后再拿起右边的筷子
要求偶数号哲学家先拿起右边的筷子,然后再拿起左边的筷子
这种方式可以保证如果相邻的两个奇偶号哲学家都想吃饭,那么只会有其中一个可以拿起筷子,另一个会直接堵塞
从而以解决死锁的问题*/
/*3.添加限制条件:仅当一个哲学家左右两根筷子都可使用时,才允许他抓起筷子*/
semaphore chopstick[5] = {1,1,1,1,1};
semaphore mutex = 1;//i号哲学家互斥的取筷子
pi() {
while(1) {
P(mutex);
P(chopstick[i]);
P(chopstick[(i+1)%5]);
V(mutex);
Have a meal;
V(chopstick[i]);
V(chopstick[(i+1)%5]);
Ponder questions;
}
}
信号量机制存在的问题导致编程程序困难、易出错,管程属于一种高级的进程同步机制(不需要再关注PV操作)
管程的组成:
管程的基本特征:
monitor ProducerConsumer {
condition full, empty;//条件变量用于实现同步(排队)
int count = 0;
void insert(Item item) {//将item放入缓冲区中
if (count == N) wait(full);
count++;
insert_item(item);
if (count == 1) signal(empty);
}
Item remove() {//从缓冲区中取出一个产品
if (count == 0) wait(empty);
count--;
if (count == N - 1) signal(full);
return remove_item();
}
}
//生产者进程
void producer() {
while(1) {
item = produce a item;
ProdecerConsumer.insert(item);
}
}
//消费者进程
void consumer() {
while(1) {
item = ProdecerConsumer.remove();
consume a item;
}
}
static class monitor {
private Item buffer[] = new Item[N];
private int count = 0;
/**
* 每次只有一个线程进行insert函数
* 如果多个线程同时调用insert函数,则后来者需要排队进行等待
* @param item
*/
public synchronized void insert(Item item) {
......
}
}
死锁的必要条件:
产生死锁必须同时满足以下4个条件:
互斥条件:只有对必须互斥使用资源的争抢,才会导致死锁现象。
不可剥夺条件:进程所获得的资源在未使用完之前,不能由其他进程强行夺走只能主动释放。
请求与保持条件:
进程已经保持了至少一个资源,但又提出了新的资源请求。
而该资源又被其他进程占有,此时请求进程被阻塞,但又对自己已有的资源保持不释放。
循环等待条件:存在一种进程资源的循环等待链,链中的每个进程已获得的资源同时被下一个进程所请求。
注:发生循环等待未必会导致死锁现象,如果同类资源大于1则即使发生循环等待也不会死锁,否则一定会发生死锁(考点)。
常见的死锁场景:
注:可以死锁发生的场景可以总结为,当不可剥夺资源的分配出现问题时,就会导致死锁现象。
注:如果系统处于安全状态就一定不会发生死锁,如果处于不安全状态就有可能会发生死锁(考点)。
银行家算法:可以在资源分配之前预先判断,这次分配是否会导致系统进入不安全状态,以此决定是否答应资源分配请求。