中途脑梗了好几次,差点寄了,不过好在这次把题留下来了(

Exam - 进程组 ipc 通信

题干

在 Linux 中,进程理论上所拥有的权限与执行它的用户的权限相同。进程运行时能够访问哪些资源或文件,不取决于进程文件的属主属组,而是取决于运行该命令的用户身份的 uid/gid,以该身份获取各种系统资源。

所以我们需要完成同一个进程组ID的不同进程的通信。具体而言,需要做到:

  1. 在 Env 结构体中添加 u_int env_gid 字段代表进程所在的进程组,初始值为 0。
  2. 实现一个修改 gid 字段的用户态函数void set_gid(u_int gid);
  3. 实现一个仅能向同组块发送消息的用户态函数int ipc_group_send(u_int whom, u_int val, const void *srcva, u_int perm);
  4. 实现 2、3 两点中对应的系统调用函数和调用接口

教程组已经把两个用户态函数实现了,我们只需要考虑系统调用函数 syscall_set_gidsyscall_ipc_try_group 即可

具体要求

  1. 在内核中为每个进程维护进程组ID,并保证每个进程创建时的的组ID为0
  2. user/include/lib.h 中:
    • 添加以下两个用户函数的声明:
    void set_gid(u_int gid);
    int ipc_group_send(u_int whom, u_int val, const void *srcva, u_int perm);
    • 添加以下两个系统调用函数的声明:
    void syscall_set_gid(u_int gid);
    int syscall_ipc_try_group_send(u_int whom, u_int val, const void *srcva, u_int perm);
  3. include/error.h 中,增加以下新错误码 E_IPC_NOT_GROUP ,表示组间通信时通信双方进程的组ID不匹配。
    #define E_IPC_NOT_GROUP 14

  4. 两个用户态函数的实现已经给出(请参看实验提供代码部分),你需要将其复制到 user/lib/ipc.c *_,具体代码的解释在*_提示部分给出。
  5. include/syscall.h 中:定义两个新的系统调用号。请注意新增系统调用号的位置,应当位于 MAX_SYSNO 之前。
  6. user/lib/syscall_lib.c 中:实现上述两个系统调用函数,发起系统调用。
  7. kern/syscall_all.c 中:添加两个系统调用在内核中的实现函数。请保证两个函数的定义位于系统调用函数表 void *syscall_table[MAX_SYSNO] 之前。
  8. kern/syscall_all.c 中的 void *syscall_table[MAX_SYSNO] 系统调用函数表中,为你定义的系统调用号添加对应的内核函数指针。
  9. 编写 syscall_ipc_try_group_send 系统调用在内核中的实现函数时,判断 -E_IPC_NOT_RECV 错误的优先级高于 -E_IPC_NOT_GROUP

实验提供代码

/* copy to user/lib/ipc.c */

void set_gid(u_int gid) {
// 你需要实现此 syscall_set_gid 系统调用
syscall_set_gid(gid);
}

int ipc_group_send(u_int whom, u_int val, const void *srcva, u_int perm) {
int r;
// 你需要实现此 syscall_ipc_try_group_send 系统调用
while ((r = syscall_ipc_try_group_send(whom, val, srcva, perm)) != 0) {
// 接受方进程尚未准备好接受消息,进程切换,后续继续轮询尝试发送请求
if (r == -E_IPC_NOT_RECV) syscall_yield();
// 接收方进程准备好接收消息,但非同组通信,消息发送失败,停止轮询,返回错误码 -E_IPC_NOT_GROUP
if (r == -E_IPC_NOT_GROUP) return -E_IPC_NOT_GROUP;
}
// 函数返回0,告知用户成功发送消息
return 0;
}

一种可行的做法

课上写了半天总是说函数名找不到,仔细一看写了一半的 group_send,写了一半的 send_group,难绷

int sys_ipc_try_group_send(u_int whom, u_int val, const void *srcva,
u_int perm) {
struct Env *e;
struct Page *p;

if (srcva != 0 && is_illegal_va((u_int)srcva)) {
return -E_INVAL;
}

if (0 != envid2env(whom, &e, 0)) {
return -E_BAD_ENV;
}

if (e->env_ipc_recving == 0) {
return -E_BAD_ENV;
}

if (e->env_ipc_recving == 0 e->env_status != ENV_NOT_RUNNABLE) {
return -E_IPC_NOT_RECV;
}

if (e->env_gid != curenv->env_gid) { // 唯一与 ipc_send 不同的地方就在这了
return -E_IPC_NOT_GROUP;
}

e->env_ipc_value = val;
e->env_ipc_from = curenv->env_id;
e->env_ipc_perm = PTE_V perm;
e->env_ipc_recving = 0;
e->env_status = ENV_RUNNABLE;
TAILQ_INSERT_TAIL(&env_sched_list, e, env_sched_link);

if (srcva != 0) {
p = page_lookup(curenv->env_pgdir, (u_int)srcva, NULL);
if (NULL == p) {
return -E_INVAL;
} else {
try(page_insert(e->env_pgdir, e->env_asid, p, e->env_ipc_dstva, perm));
}
}
return 0;
}

void sys_set_gid(u_int gid) { // 简单赋值
curenv->env_gid = gid;
return;
}

思路

按照题目给的思路顺下来其实很容易就能写完,顺便也可以用这个题回顾一下系统调用:怎样添加一个新的、可用的系统调用?

  • 首先从内核态出发,编写一个能够完成功能的函数,看一下它都需要什么参数 - kern/syscall_all.c
  • 然后在 void *syscall_table[MAX_SYSNO] 把函数添加进去,使得 do_syscall 函数能跳到这个新函数里:这需要随便写一个字符串当成系统调用号,无所谓了 - kern/syscall_all.c
  • 找到刚才那个系统调用号的枚举类,把定义加上 - include/syscall.h
  • do_syscall 不需要变化,然后再上一层是 msyscall,它需要一个 syscall_ 开头的函数进行调用。到这里我们就完成了内核态中所需要做的所有改动
  • 回到用户态,编写一个用户态的 syscall_new 函数调用 msyscall,同时需要注意参数的第一个参数需要是刚才加进去的调用号 - users/lib/syscall_lib.c
  • 最后编写顶层的用户态函数,其中调用 syscall_new 函数,用户态工作也就完成了 - users/某个文件
  • 最最后别忘了加上函数声明:内核态不需要,用户态加在 users/include/lib.h 即可

Extra - 家族 ipc 广播

题干

课下我们在 MOS 系统中实现了进程间通信。

现在你需要仿照 ipc_send 函数在 user/lib/ipc.c 中实现 ipc_broadcast 函数,使得调用 ipc_broadcast 可以使当前进程向其后代进程(也即当前广播进程的子进程、子进程的子进程、子进程的子进程的子进程…以此类推)发起广播消息,当后代进程进入 recv 后进行发送。

具体要求

ipc_broadcast

需要在 user/lib/ipc.c 新增:

void ipc_broadcast(u_int val, void * srcva, u_int perm);

参数

  • val :进程广播传递的具体数值, 与 ipc_send 函数中的定义相同。
  • srcva :进程广播发送页的对应用户虚地址,与 ipc_send 函数中的定义相同。
  • perm : 传递的页面的权限位设置,与 ipc_send 函数中的定义相同。

注意点

  • 你可以实现 syscall_ipc_try_broadcast 系统调用,使其行为类似于 syscall_ipc_try_send,但尝试发送给当前进程的所有后代进程。
  • 你也可以尝试在用户空间利用 envs 实现相关行为。
  • 发送广播消息时,你可以先等待所有后代进程进入接受状态,再统一进行实际传输,也可以依次等待每个后代进程,一旦其处于接受状态,当即对其进行实际传输。

也就是说可以在用户态使用这些已有的调用函数完成目标,也可以像 Exam 中添加一种新的系统调用处理这种请求。

如果注意到原来提供的 ipc_send 函数能使用 bfs 操作进程块数组,那实际上难度就会降低很多。无所谓,我没看出来

两种可行的做法

新建系统调用 - sys_ipc_broadcast

类似在刚才 Exam 中提到的思路,加一个新的系统调用

int sys_ipc_broadcast(u_int val, void *srcva, u_int perm) {
u_int childs[20];
for (int i = 0; i < 20; i++) {
childs[i] = 0;
}
struct Env *e;
struct Page *p;

// printk("childs ready!\n");

if (srcva != 0 && is_illegal_va((u_int)srcva)) {
return -E_INVAL;
}

/* Step1: 找到直系的子进程 */
for (int i = 0; i < NENV; i++) {
if (envs[i].env_parent_id == curenv->env_id) {
for (int j = 0; j < 20; j++) {
if (childs[j] == 0) {
childs[j] = envs[i].env_id;
break;
}
}
}
}

/* Step2: 通过 bfs 找到所有子进程的子进程 */
for (int i = 0; childs[i] != 0; i++) {
for (int j = 0; j < NENV; j++) {
if (envs[j].env_parent_id == childs[i]) {
for (int k = 0; k < 20; k++) {
if (childs[k] == envs[j].env_id) {
break;
}
if (childs[k] == 0) {
childs[k] = envs[j].env_id;
break;
}
}
}
}
}

/* Step3: 对所有待发送的进程进行发送 */
for (int i = 0; childs[i] != 0; i++) {
// printk("%d: %x\n", i, childs[i]);
sys_ipc_try_send(childs[i], val, srcva, perm);
}

return 0;
}

用户态通过已有函数处理

因为代码不是我的,所以我就不贴了,说一说思路吧

首先和第一种做法一样,需要用循环和队列 bfs 出所有满足条件的进程 env_id,最后实际上可以直接用 ipc_send 解决,太快了。

一点废话

其实一开始读这个题我理解成调用 env_alloc 函数来创建进程了,然后就在 Env 里面加了一个数组字段保存自己的孩子,同时在 env_alloc 里通过 env_parent_id 更新所有祖宗进程的字段,最后在系统调用进行 send 的时候直接查一下自己的字段就能跑了

结果这个题它创建进程最后用的是宏 ENV_CREATE_PRIORITY,也就是调用了 env_create 函数。甚至 parent_id 是下面这么加进去的!看起来两个函数好像没啥差别,但是 env_create 函数只能产生 parent_id = 0 的新进程,我这一套直接寄了

struct Env *ppa1 = ENV_CREATE_PRIORITY(test_ppa, 5);
struct Env *ppa2 = ENV_CREATE_PRIORITY(test_ppa, 5);
struct Env *ppa3 = ENV_CREATE_PRIORITY(test_ppa, 5);
struct Env *ppa4 = ENV_CREATE_PRIORITY(test_ppa, 5);

ppa2->env_parent_id = ppa1->env_id;
ppa3->env_parent_id = ppa1->env_id;
ppa4->env_parent_id = ppa3->env_id;

幸亏看了一眼测试代码,要不寄大发了。不过反正写完这一版才发现写的不对,实际上已经寄了。

不然我觉得我那个做法将能算得上是绝杀(可能吧)

总之 lab4 这样就算结束一半了,之后再看吧。