持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第23天,点击查看活动详情
前言
这篇文章又拖了好久的一个笔记,终于又能填一片坑了。
从技术上讲,在JOS中,这是“环境间通信”或“IEC”,但其他人都称之为IPC,所以我们将使用标准术语。)
我们一直关注操作系统的隔离方面,它提供了一种错觉,即每个程序都有自己的机器。操作系统的另一个重要服务是允许程序在需要时相互通信。让程序与其他程序交互可能会非常强大。Unix管道模型是典型的示例。进程间通信有许多模型。即使在今天,关于哪种模型是最好的仍然存在争议。我们不会参加那场辩论。相反,我们将实现一个简单的IPC机制,然后进行尝试。
JOS中的IPC
您将实现几个额外的JOS内核系统调用,它们共同提供了一个简单的进程间通信机制。您将实现两个系统调用,sys_ipc_recv和sys_ipc_try_send。然后,您将实现两个库包装器ipc_recv和ipc_send。用户环境可以使用JOS的IPC机制相互发送的“消息”由两个组件组成:一个32位的值,以及可选的单页映射。允许环境在消息中传递页面映射提供了一种有效的方法,可以传输比单个32位整数更大的数据,还允许环境简单地设置共享内存。
发送和接收消息
为了接收消息,环境调用sys_ipc_recv。此系统调用取消了当前环境的调度,在收到消息之前不会再次运行它。当一个环境正在等待接收消息时,任何其他环境都可以向其发送消息——不仅仅是特定环境,也不仅仅是与接收环境有父/子安排的环境。换言之,您在A部分中实现的权限检查将不适用于IPC,因为IPC系统调用是经过精心设计的,以确保“安全”:一个环境不能仅仅通过发送消息就导致另一个环境发生故障(除非目标环境也有故障)。
为了尝试发送一个值,环境使用接收方的环境id和要发送的值调用sys_ipc_try_send。如果命名环境实际正在接收(它已调用sys_ipc_recv,但尚未获得值),则send将传递消息并返回0。否则,send将返回-E_ipc_not_recv,表示目标环境当前不希望接收值。用户空间中的库函数ipc_recv将负责调用sys_ipc_recv,然后在当前环境的struct Env中查找有关接收值的信息。
类似地,库函数ipc_send将负责重复调用sys_ipc_try_send,直到发送成功。
页面传输
当环境使用有效的dstva参数(UTOP以下)调用sys_ipc_recv时,环境表示愿意接收页面映射。如果发送方发送一个页面,那么该页面应该映射到接收方地址空间中的dstva。如果接收器已经在dstva上映射了一个页面,则取消映射前一个页面。
当一个环境使用有效的srcva(低于UTOP)调用sys_ipc_try_send时,这意味着发送方希望将当前映射在srcval的页面发送给接收方,并具有perm权限。IPC成功后,发送方将其位于srcva的页面的原始映射保留在其地址空间中,但接收方也会在接收方最初指定的dstva的接收方地址空间中获得相同物理页面的映射。因此,此页面将在发送方和接收方之间共享。
如果发送方或接收方均未指明应转移页面,则不会转移页面。在任何IPC之后,内核都会将接收方的env结构中的新字段env_ipc_perm设置为所接收页面的权限,如果没有收到页面,则设置为零。
实现IPC
Exercise 15. Implement sys_ipc_recv and sys_ipc_try_send in kern/syscall.c. Read the comments on both before implementing them, since they have to work together. When you call envid2env in these routines, you should set the checkperm flag to 0, meaning that any environment is allowed to send IPC messages to any other environment, and the kernel does no special permission checking other than verifying that the target envid is valid.Then implement the ipc_recv and ipc_send functions in lib/ipc.c.
static int
sys_ipc_try_send(envid_t envid, uint32_t value, void *srcva, unsigned perm)
{
// LAB 4: Your code here.
struct Env *env;
if (envid2env(envid, &env, 0) < 0) return -E_BAD_ENV;
if (!env->env_ipc_recving) return -E_IPC_NOT_RECV;
if ((size_t) srcva < UTOP) {
if (((size_t) srcva % PGSIZE) != 0) {
return -E_INVAL;
}
if ((perm & PTE_U) != PTE_U || (perm & PTE_P) != PTE_P) return -E_INVAL;
pte_t *pte;
struct PageInfo *pp = page_lookup(curenv->env_pgdir, srcva, &pte);
if (!pp) return -E_INVAL;
if ((perm & PTE_W) && ((size_t) *pte & PTE_W) != PTE_W) return -E_INVAL;
if ((size_t) env->env_ipc_dstva < UTOP) {
if (page_insert(env->env_pgdir, pp, env->env_ipc_dstva, perm) < 0) return -E_NO_MEM;
env->env_ipc_perm = perm;
}
} else {
env->env_ipc_perm = 0;
}
env->env_ipc_from = curenv->env_id;
env->env_ipc_recving = 0;
env->env_ipc_value = value;
env->env_status = ENV_RUNNABLE;
env->env_tf.tf_regs.reg_eax = 0;
return 0;
}
这个函数主要执行以下功能:
-
如果srcva<UTOP,则发送当前映射到srcva的页面
-
如果发送成功:
- 更新env_ipc_recving为0用来阻止接下来的发送
- 更新env_ipc_form为envid
- 更新env_ipc_value为传入value的参数
- 如果页面已经传输,则设置env_ipc_perm为传入的perm的参数,否则应设置为0
- 将目标环境标记为RUNNABLE,将来自暂停的sys_ipc_recv系统调用的返回值设置为0
-
发送失败的情况包括:
- 如果当前envid不存在,返回E_BAD_ENV
- 如果env_ipc_recving状态没有被阻塞或者其他环境先传输了,返回E_IPC_NOT_RECV
- 如果srcva<UTOP并且不是页面对齐的,返回E_INVAL
- 如果srcva<UTOP并且权限perm错误,返回E_INVAL
- 如果srcva<UTOP但是并没有查找到映射,返回E_INVAL
- 如果srcva在当前环境是只读的,但是拥有了写权限,返回E_INVAL
- 如果没有足够的内存来映射envid的srcva,则返回E_NO_MEM
static int
sys_ipc_recv(void *dstva)
{
// LAB 4: Your code here.
if ((size_t) dstva < UTOP && ((size_t) dstva % PGSIZE) != 0) return -E_INVAL;
curenv->env_ipc_recving = 1;
curenv->env_ipc_dstva = dstva;
curenv->env_status = ENV_NOT_RUNNABLE;
sys_yield();
return 0;
}
该函数主要执行:
- 一直阻塞,直到一个消息到达
- 使用env_ipc_recving和env_ipc_dstva记录要接受的数据
- 将当前环境标记为ENV_NOT_RUNNABLE,自动放弃当前CPU
- 如果映射发送页的虚拟地址dstva<UTOP,则表示愿意接受一页数据
- 该函数应该仅在出错时(dstva<UTOP并且页面未对弃的情况下返回E_INVAL)返回值,但是系统调用成功将会返回0
int32_t
ipc_recv(envid_t *from_env_store, void *pg, int *perm_store)
{
// LAB 4: Your code here.
if (!pg) pg = (void *)-1;
int state = sys_ipc_recv(pg);
if (state < 0) {
if (from_env_store) *from_env_store = 0;
if (perm_store) *perm_store = 0;
return state;
}
if (from_env_store) *from_env_store = thisenv->env_ipc_from;
if (perm_store) *perm_store = thisenv->env_ipc_perm;
return thisenv->env_ipc_value;
}
这个函数主要:
- 接受ipc的值并返回
- 如果传入地址非空,则把发送方的envid存储在from_env_store中,把权限存储在perm_store中(如果页面转发成功则为非0值)
- 如果系统调用失败则把perm_store和from_env_store全部置0并且返回错误,否则返回发送方的env_ipc_value
void
ipc_send(envid_t to_env, uint32_t val, void *pg, int perm)
{
// LAB 4: Your code here.
if (!pg) {
pg = (void *)-1;
perm = 0;
}
int state;
while (1) {
state = sys_ipc_try_send(to_env, val, pg, perm);
if (state == 0) return;
if (state != -E_IPC_NOT_RECV) {
panic("ipc_send: %e", state);
}
sys_yield();
}
}
这个函数主要:
- 如果页面非空,则把val、pg和perm发送给toenv
- 这个函数会一直尝试发送直到发送成功
- 在除了-E_IPC_NOT_RECV错误以外的任何错误都将导致panic
- 当pg为空时,也向sys_ipc_try_send传递一个可以被解释的错误值表示页面为空