进程间通信(IPC) 本节内容讲解 ChCore 关于进程间通信(IPC)部分的源码
教材为我们讲解了 IPC 机制的历史演化:从简单 IPC 机制到共享内存/内核辅助,再到后来的 LRPC,L4 等进程间通信等模型。而 ChCore 的设计则是“择优取之”:
消息传递与通知 :基于 LRPC 的迁移线程技术+L4 微内核的直接进程切换技术
数据传输 :基于用户态共享内存
整体上看,便是将 IPC 的两端进程分为 Client 和 Server,注册 IPC 服务后通过能力组机制建立连接,之后的通信过程则通过 syscall 来进行,而具体实现则通过上述机制
下面以一次完整的 IPC 注册-调用的过程,来讲解 IPC 源码的设计与其机制的实现
IPC 服务端注册 要使用 IPC 功能,首先需要进行服务器端的注册。对于扮演服务器端的进程来说,它需要调用 ipc_register_server
来声明自己为 IPC 的服务器端。先来看它的 API 接口:
1 2 int ipc_register_server (server_handler server_handler, void *(*client_register_handler)(void *))
其接受两个参数,分别是两个函数,其作用为
client_register_handler
:为服务端提供的用于注册的回调函数,用于处理 client 注册。例如校验发出注册请求的 client 是否具有相应的 capability 等
server_handler
:服务器端的服务回调函数,在成功注册之后处理 LRPC 调用,也就是处理迁移线程模型之中,服务器端提供的“被迁移的”代码段
这里的 server_handler 定义如下:
1 2 3 4 5 6 typedef void (*server_handler) ( void *shm_ptr, unsigned int max_data_len, unsigned int send_cap_num, badge_t client_badge ) ;
然后再来看注册函数的代码:
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 int ipc_register_server (server_handler server_handler, void *(*client_register_handler)(void *)) { return ipc_register_server_with_destructor( server_handler, client_register_handler, DEFAULT_DESTRUCTOR); }int ipc_register_server_with_destructor (server_handler server_handler, void *(*client_register_handler)(void *), server_destructor server_destructor) { cap_t register_cb_thread_cap; int ret;#define ARG_SET_BY_KERNEL 0 pthread_t handler_tid; register_cb_thread_cap = chcore_pthread_create_register_cb(&handler_tid, NULL , client_register_handler, (void *)ARG_SET_BY_KERNEL); BUG_ON(register_cb_thread_cap < 0 ); ret = usys_register_server((unsigned long )server_handler, (unsigned long )register_cb_thread_cap, (unsigned long )server_destructor); if (ret != 0 ) { printf ("%s failed (retval is %d)\n" , __func__, ret); } return ret; }
此处做了一个带 destructor 的包装,不过这里传入的默认的 destructor 是个 NULL,暂时先不用管
这里实际上做了两件事,调用了两个函数:
创建 Client 注册回调线程:通过 chcore_pthread_create_register_cb
函数实现,返回值为创造的线程的能力
该线程是被动线程,负责处理 Client 端端 IPC 注册请求
线程类型为 TYPE_REGISTER
,平时不会被调度执行
只有当 IPC 客户端需要注册时该线程才运行,负责初始化 IPC 连接
通过系统调用注册 Server 端:即 usys_register_server
函数,它实际上就是系统调用
下面分别展开讲解
创建 Client 注册回调线程 核心便是 chcore_pthread_create_register_cb
函数,源代码 200 多行这里就不放了,主要做的还是创建线程那一套:
分配线程栈空间
设置线程 TLS
设置线程的回调特性
将线程添加到线程链表
关键便在于第三点,我们将回调函数作为参数传入了 chcore_pthread_create_register_cb
,而它直接被设置为新线程的 entry:
1 2 3 4 5 6 7 8 9 10 11 12 13 struct thread_args _args ; _args.cap_group_cap = 0 ; _args.stack = (unsigned long )stack ; _args.pc = (type != TYPE_USER ? (unsigned long )entry : (unsigned long )(c11 ? start_c11 : start)); _args.arg = (type != TYPE_USER ? (unsigned long )arg : (unsigned long )args); _args.prio = attr._a_sched? attr._a_prio: CHILD_THREAD_PRIO; _args.tls = (unsigned long )TP_ADJ(new); _args.type = type; _args.clear_child_tid = (int *)&__thread_list_lock; ret = new->tid = usys_create_thread((unsigned long )&_args);
忘了这是什么东西?回忆一下结构体组成即可:
1 2 3 4 5 6 7 8 9 10 11 struct thread_args { cap_t cap_group_cap; vaddr_t stack ; vaddr_t pc; unsigned long arg; vaddr_t tls; unsigned int prio; unsigned int type; int *clear_child_tid; };
随后便用传入的参数通过系统调用创建线程了
通过系统调用注册 Server 端 直接看 usys_register_server
:
1 2 3 4 5 6 7 8 9 int usys_register_server (unsigned long callback, cap_t register_thread_cap, unsigned long destructor) { return chcore_syscall3(CHCORE_SYS_register_server, callback, register_thread_cap, destructor); }
顺藤摸瓜找到真正被调用的 syscall sys_register_server
:
1 2 3 4 5 6 7 8 9 int sys_register_server (unsigned long ipc_routine, cap_t register_thread_cap, unsigned long destructor) { return register_server( current_thread, ipc_routine, register_thread_cap, destructor); }static int register_server (struct thread *server, unsigned long ipc_routine, cap_t register_thread_cap, unsigned long destructor)
这里已经嵌套多层了,注意各个参数:
server
:代表服务器线程,在这里即为 current_thread
,指的是服务器进程端的主线程(可以参考 Lab 文档)
ipc_routine
:服务回调函数,即为前面的 server_handler
register_thread_cap
:注册回调线程的能力
destructor
:析构函数,这里还是 null
接下来就看看这个函数做了什么:
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 static int register_server (struct thread *server, unsigned long ipc_routine, cap_t register_thread_cap, unsigned long destructor) { struct ipc_server_config *config ; struct thread *register_cb_thread ; struct ipc_server_register_cb_config *register_cb_config ; BUG_ON(server == NULL ); if (server->general_ipc_config != NULL ) { kdebug("A server thread can only invoke **register_server** once!\n" ); return -EINVAL; } register_cb_thread = obj_get(current_cap_group, register_thread_cap, TYPE_THREAD); if (!register_cb_thread) { kdebug("A register_cb_thread is required.\n" ); return -ECAPBILITY; } if (register_cb_thread->thread_ctx->type != TYPE_REGISTER) { kdebug("The register_cb_thread should be TYPE_REGISTER!\n" ); obj_put(register_cb_thread); return -EINVAL; } config = kmalloc(sizeof (*config)); if (!config) { obj_put(register_cb_thread); return -ENOMEM; } config->declared_ipc_routine_entry = ipc_routine; config->register_cb_thread = register_cb_thread; register_cb_config = kmalloc(sizeof (*register_cb_config)); if (!register_cb_config) { kfree(config); obj_put(register_cb_thread); return -ENOMEM; } register_cb_thread->general_ipc_config = register_cb_config; lock_init(®ister_cb_config->register_lock); register_cb_config->register_cb_entry = arch_get_thread_next_ip(register_cb_thread); register_cb_config->register_cb_stack = arch_get_thread_stack(register_cb_thread); register_cb_config->destructor = destructor; obj_put(register_cb_thread);#if defined(CHCORE_ARCH_AARCH64) smp_mb();#else #endif server->general_ipc_config = config; return 0 ; }
注册的过程就是配置 config
,这里一共配置了两个线程的 config
,分别是主线程和注册回调线程。看看结构体的成员:
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 struct ipc_server_config { struct thread *register_cb_thread ; unsigned long declared_ipc_routine_entry; };struct ipc_server_register_cb_config { struct lock register_lock ; vaddr_t register_cb_entry; vaddr_t register_cb_stack; vaddr_t destructor; cap_t conn_cap_in_client; cap_t conn_cap_in_server; cap_t shm_cap_in_server; };
对服务器主线程来说,注册的过程为其 config 配置了注册回调线程和服务线程的入口函数,即前面的 server_handler ;对注册回调线程来说,它需要保存自己的 PC 和 SP 等信息,为后面的线程迁移做准备。最后处理 TLB,并设置 server 主线程的相应字段,结束注册过程
这时候 Server 端注册已经完成,下一步是注册客户端并建立 IPC 连接
IPC 客户端注册 外层代码与整体逻辑 上述注册服务端的操作皆在服务器进程上完成,而之后则进行 IPC 的客户端注册与连接,是在 IPC 客户端线程上执行函数 完成的,我们先看源码:
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 ipc_struct_t *ipc_register_client (cap_t server_thread_cap) { cap_t conn_cap; ipc_struct_t *client_ipc_struct; struct client_shm_config shm_config ; cap_t shm_cap; client_ipc_struct = malloc (sizeof (ipc_struct_t )); if (client_ipc_struct == NULL ) { return NULL ; } shm_cap = usys_create_pmo(IPC_PER_SHM_SIZE, PMO_DATA); if (shm_cap < 0 ) { printf ("usys_create_pmo ret %d\n" , shm_cap); goto out_free_client_ipc_struct; } shm_config.shm_cap = shm_cap; shm_config.shm_addr = chcore_alloc_vaddr(IPC_PER_SHM_SIZE); while (1 ) { conn_cap = usys_register_client(server_thread_cap, (unsigned long )&shm_config); if (conn_cap == -EIPCRETRY) { usys_yield(); } else if (conn_cap < 0 ) { printf ("client: %s failed (return %d), server_thread_cap is %d\n" , __func__, conn_cap, server_thread_cap); goto out_free_vaddr; } else { break ; } } client_ipc_struct->lock = 0 ; client_ipc_struct->shared_buf = shm_config.shm_addr; client_ipc_struct->shared_buf_len = IPC_PER_SHM_SIZE; client_ipc_struct->conn_cap = conn_cap; return client_ipc_struct; out_free_vaddr: usys_revoke_cap(shm_cap, false ); chcore_free_vaddr(shm_config.shm_addr, IPC_PER_SHM_SIZE); out_free_client_ipc_struct: free (client_ipc_struct); return NULL ; }
这部分要干的活就多了:
通过 syscall 申请一块共享内存
执行另一个 syscall,完成客户端共享内存的映射,这里还会进入之前创建好的注册回调线程,创建服务线程,处理服务端的相应信息
最后设置 IPC 结构体的字段并返回之
和之前服务器端的注册函数一样,这里的 usys_create_pmo
和 usys_register_client
也是系统调用的一层包装,我们需要重点关注的是后者,它将进一步涉及到注册回调线程的相关操作
客户端注册的 syscall 源码如下所示,可以参考注释理解函数内容
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 cap_t sys_register_client (cap_t server_cap, unsigned long shm_config_ptr) { struct thread *client ; struct thread *server ; struct client_shm_config shm_config = {0 }; int r; struct client_connection_result res ; struct ipc_server_config *server_config ; struct thread *register_cb_thread ; struct ipc_server_register_cb_config *register_cb_config ; client = current_thread; server = obj_get(current_cap_group, server_cap, TYPE_THREAD); if (!server) { r = -ECAPBILITY; goto out_fail; } server_config = (struct ipc_server_config *)(server->general_ipc_config); if (!server_config) { r = -EIPCRETRY; goto out_fail; } register_cb_thread = server_config->register_cb_thread; register_cb_config = (struct ipc_server_register_cb_config *)(register_cb_thread->general_ipc_config); if (try_lock(®ister_cb_config->register_lock) != 0 ) { r = -EIPCRETRY; goto out_fail; } if (check_user_addr_range(shm_config_ptr, sizeof (shm_config) != 0 )) { r = -EINVAL; goto out_fail_unlock; } r = copy_from_user((void *)&shm_config, (void *)shm_config_ptr, sizeof (shm_config)); if (r) { r = -EINVAL; goto out_fail_unlock; } r = map_pmo_in_current_cap_group( shm_config.shm_cap, shm_config.shm_addr, VMR_READ | VMR_WRITE); if (r != 0 ) { goto out_fail_unlock; } r = create_connection( client, server, shm_config.shm_cap, shm_config.shm_addr, &res); if (r != 0 ) { goto out_fail_unlock; } register_cb_config->conn_cap_in_client = res.client_conn_cap; register_cb_config->conn_cap_in_server = res.server_conn_cap; register_cb_config->shm_cap_in_server = res.server_shm_cap; thread_set_ts_blocking(current_thread); arch_set_thread_stack(register_cb_thread, register_cb_config->register_cb_stack); arch_set_thread_next_ip(register_cb_thread, register_cb_config->register_cb_entry); arch_set_thread_arg0(register_cb_thread, server_config->declared_ipc_routine_entry); obj_put(server); register_cb_thread->thread_ctx->sc = current_thread->thread_ctx->sc; sched_to_thread(register_cb_thread); BUG_ON(1 ); out_fail_unlock: unlock(®ister_cb_config->register_lock); out_fail: if (server) obj_put(server); return r; }
这个注册函数的大体流程即为
在客户端映射共享内存
从当前 thread 的 cap_group
里面找到传入的 server_cap
对应的 slot,进而得到 server 线程对象
从 server 获取它的 ipc_config
拿锁,避免并发问题
检查 client 声明的共享内存地址,并将之拷贝到内核态,再建立共享内存上的映射
创建 ipc_connection
对象,并把 cap 给到 server 和 client
切换到注册回调线程
设置好调用参数,栈寄存器,异常处理寄存器
这一部分用到了注册 server 时配置好的 config
调用 sched 切换控制权给 server 的注册回调线程,并进入设置好的注册回调函数
关于创建 ipc_connection
的过程,可以参考 create_connection
函数,配置好的结构体如下:
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 struct ipc_connection *conn = { .current_client_thread = client_thread, .server_handler_thread = NULL , .client_badge = current_cap_group->badge, .client_pid = current_cap_group->pid, .shm = { .client_shm_uaddr = shm_addr_client, .server_shm_uaddr = 0 , .shm_size = shm_size, .shm_cap_in_client = shm_cap_client, .shm_cap_in_server = shm_cap_server }, .ownership = LOCK_INIT_VAL, .conn_cap_in_client = conn_cap, .conn_cap_in_server = server_conn_cap, .state = CONN_INCOME_STOPPED, .server_cap_buf = { [0 ... MAX_CAP_TRANSFER-1 ] = { .valid = false , .cap = 0 , .mask = 0 , .rest = 0 } }, .client_cap_buf = { [0 ... MAX_CAP_TRANSFER-1 ] = { .valid = false , .cap = 0 , .mask = 0 , .rest = 0 } } };
最后再将这个 conn 配置给 struct client_connection_result res
,存储起来即可:
1 2 3 4 5 6 7 8 struct client_connection_result { cap_t client_conn_cap; cap_t server_conn_cap; cap_t server_shm_cap; struct ipc_connection *conn ; };
然后 sys_register_client
的历史使命就结束了,它把尚未完成的任务交给了注册回调线程
注册回调函数
注册回调线程运行的入口函数为主线程调用ipc_register_server
是提供的 client_register_handler 参数,一般会使用默认的DEFAULT_CLIENT_REGISTER_HANDLER
宏定义的入口函数,即定义在user/chcore-libc/musl-libc/src/chcore-port/ipc.c
中的register_cb
根据 Lab 文档的指引我们来到 register_cb
的地盘,还记得这个 client_register_handler
是干啥的不?它在服务端主线程创建注册回调线程的时候被设置为了注册回调线程的入口。那么在注册客户端函数将线程切换过来的时候(注意这里用的切换函数是 sched_to_thread
),便会执行 register_cb
的代码
那么它又会肩负怎样的 IPC 使命呢?且看代码(注意现在已经是 server 端进程下的注册回调线程了):
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 void *register_cb (void *ipc_handler) { cap_t server_thread_cap = 0 ; unsigned long shm_addr; shm_addr = chcore_alloc_vaddr(IPC_PER_SHM_SIZE); pthread_t handler_tid; server_thread_cap = chcore_pthread_create_shadow( &handler_tid, NULL , ipc_handler, (void *)NO_ARG ); BUG_ON(server_thread_cap < 0 ); #ifndef CHCORE_ARCH_X86_64 ipc_register_cb_return( server_thread_cap, (unsigned long )ipc_shadow_thread_exit_routine, shm_addr );#else ipc_register_cb_return( server_thread_cap, (unsigned long )ipc_shadow_thread_exit_routine_naked, shm_addr );#endif return NULL ; }
创建线程的部分和之前如出一辙,只是注意这里的服务线程同样是没有调度上下文的影子线程
忘记什么是服务线程了?回看 Lab 文档:
ChCore 的 IPC 接口不是传统的 send/recv 接口。其更像客户端/服务器模型,其中 IPC 请求接收者是服务器,而 IPC 请求发送者是客户端。 服务器进程中包含三类线程:
主线程:该线程与普通的线程一样,类型为TYPE_USER
。该线程会调用ipc_register_server
将自己声明为一个 IPC 的服务器进程,调用的时候会提供两个参数:服务连接请求的函数 client_register_handler 和服务真正 IPC 请求的函数 server_handler(即图中的ipc_dispatcher
),调用该函数会创建一个注册回调线程;
注册回调线程:该线程的入口函数为上文提到的 client_register_handler,类型为TYPE_REGISTER
。正常情况下该线程不会被调度执行,仅当有 Client 发起建立 IPC 连接的请求时,该线程运行并执行 client_register_handler,为请求建立连接的 Client 创建一个服务线程(即图中的 IPC handler thread)并在服务器进程的虚拟地址空间中分配一个可以用来映射共享内存的虚拟地址。
服务线程:当 Client 发起建立 IPC 连接请求时由注册回调线程创建,入口函数为上文提到的 server_handler,类型为TYPE_SHADOW
。正常下该线程不会被调度执行,仅当有 Client 端线程使用ipc_call
发起 IPC 请求时,该线程运行并执行 server_handler(即图中的ipc_dispatcher
),执行结束之后会调用ipc_return
回到 Client 端发起 IPC 请求的线程。
还记得 sys_register_client
尚未完成的历史使命吗?现在共享内存还只是在 client 端做好了映射,server 端目前仅有一个申请好的虚拟地址;同时 struct ipc_connection
也还有部分关于 server 的字段没有填写,于是这里的 ipc_register_cb_return
将接过 IPC 的接力棒:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 void ipc_register_cb_return ( cap_t server_thread_cap, unsigned long server_thread_exit_routine, unsigned long server_shm_addr) { usys_ipc_register_cb_return( server_thread_cap, server_thread_exit_routine, server_shm_addr ); }
虚晃一枪,原来是 syscall 套壳,我们继续前进:
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 int sys_ipc_register_cb_return ( cap_t server_handler_thread_cap, unsigned long server_thread_exit_routine, unsigned long server_shm_addr) { struct ipc_server_register_cb_config *config ; struct ipc_connection *conn ; struct thread *client_thread ; struct thread *ipc_server_handler_thread ; struct ipc_server_handler_config *handler_config ; int r = -ECAPBILITY; config = (struct ipc_server_register_cb_config *) current_thread->general_ipc_config; if (!config) goto out_fail; conn = obj_get( current_cap_group, config->conn_cap_in_server, TYPE_CONNECTION ); if (!conn) goto out_fail; ipc_server_handler_thread = (struct thread *)obj_get( current_cap_group, server_handler_thread_cap, TYPE_THREAD ); if (!ipc_server_handler_thread) goto out_fail_put_conn; r = map_pmo_in_current_cap_group( config->shm_cap_in_server, server_shm_addr, VMR_READ | VMR_WRITE ); if (r != 0 ) goto out_fail_put_thread; client_thread = conn->current_client_thread; arch_set_thread_return(client_thread, config->conn_cap_in_client); if (!ipc_server_handler_thread->general_ipc_config) { handler_config = (struct ipc_server_handler_config *)kmalloc( sizeof (*handler_config)); if (!handler_config) { r = -ENOMEM; goto out_fail_put_thread; } ipc_server_handler_thread->general_ipc_config = handler_config; lock_init(&handler_config->ipc_lock); handler_config->ipc_routine_entry = arch_get_thread_next_ip(ipc_server_handler_thread); handler_config->ipc_routine_stack = arch_get_thread_stack(ipc_server_handler_thread); handler_config->ipc_exit_routine_entry = server_thread_exit_routine; handler_config->destructor = config->destructor; } obj_put(ipc_server_handler_thread); conn->shm.server_shm_uaddr = server_shm_addr; conn->server_handler_thread = ipc_server_handler_thread; conn->state = CONN_VALID; conn->current_client_thread = NULL ; conn->conn_cap_in_client = config->conn_cap_in_client; conn->conn_cap_in_server = config->conn_cap_in_server; obj_put(conn); thread_set_ts_waiting(current_thread); unlock(&config->register_lock); current_thread->thread_ctx->sc = NULL ; sched_to_thread(client_thread); out_fail_put_thread: obj_put(ipc_server_handler_thread); out_fail_put_conn: obj_put(conn); out_fail: return r; }
最后返回一个连接信息的 ipc_struct
,即 IPC 控制块(后文的 ICB),并将线程切换回 client 线程,结束注册过程
总体来说,干了三件事:
完成共享内存的创建以及在两边进程的映射
创建存储信息的相关结构体并完善其内容
通过注册回调线程创建了第一个待命的服务线程
至此 IPC 的准备工作结束,下面开始正式的 ipc_call
IPC Call 向共享内存填充数据 首先,client 会调用 ipc_create_msg
和 ipc_set_msg_data
向共享内存(前面注册时候和 server“沟通完毕”)填充数据,之后将前面注册过程的 ipc_struct
和 ipc_create_msg
得到的 msg 作为参数,调用 ipc_call
向共享内存填写数据的代码如下:
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 ipc_msg_t *ipc_create_msg (ipc_struct_t *icb, unsigned int data_len) { return ipc_create_msg_with_cap(icb, data_len, 0 ); }ipc_msg_t *ipc_create_msg_with_cap (ipc_struct_t *icb, unsigned int data_len, unsigned int send_cap_num) { BUILD_BUG_ON(sizeof (ipc_msg_t ) > SERVER_IPC_MSG_BUF_SIZE); ipc_msg_t *ipc_msg; unsigned long buf_len; if (unlikely(icb->conn_cap == 0 )) { if (connect_system_server(icb) != 0 ) { printf ("connect ipc server failed!\n" ); exit (-1 ); } } chcore_spin_lock(&(icb->lock)); buf_len = icb->shared_buf_len; if (data_len > buf_len) { printf ("%s failed: too long msg (the usable shm size is 0x%lx)\n" , __func__, buf_len); goto out_unlock; } ipc_msg = (ipc_msg_t *)malloc (sizeof (ipc_msg_t )); if (!ipc_msg) { goto out_unlock; } ipc_msg->data_ptr = SHM_PTR_TO_CUSTOM_DATA_PTR(icb->shared_buf); ipc_msg->max_data_len = buf_len; ipc_msg->send_cap_num = send_cap_num; ipc_msg->response_hdr = (struct ipc_response_hdr *)icb->shared_buf; ipc_msg->icb = icb; ipc_msg->thread_type = THREAD_CLIENT; return ipc_msg; out_unlock: chcore_spin_unlock(&(icb->lock)); printf ("ipc create msg failed!\n" ); exit (-1 ); }int ipc_set_msg_data (ipc_msg_t *ipc_msg, void *data, unsigned int offset, unsigned int len) { if ((offset + len < offset) || (offset + len > ipc_msg->max_data_len)) { printf ("%s failed due to overflow.\n" , __func__); return -1 ; } memcpy (ipc_get_msg_data(ipc_msg) + offset, data, len); return 0 ; }
ipc_call 整个 ipc_call
也是一层系统调用的包装:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 long ipc_call (ipc_struct_t *icb, ipc_msg_t *ipc_msg) { long ret; if (unlikely(icb->conn_cap == 0 )) { if ((ret = connect_system_server(icb)) != 0 ) return ret; } do { ret = usys_ipc_call( icb->conn_cap, ipc_get_msg_send_cap_num(ipc_msg) ); } while (ret == -EIPCRETRY); return ret; }
继续前进找到系统调用的实现:
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 unsigned long sys_ipc_call (cap_t conn_cap, unsigned int cap_num) { struct ipc_connection *conn ; int r = 0 ; if (cap_num > MAX_CAP_TRANSFER) { return -EINVAL; } conn = obj_get(current_cap_group, conn_cap, TYPE_CONNECTION); if (unlikely(!conn)) { return -ECAPBILITY; } if (try_lock(&conn->ownership) == 0 ) { if (conn->state != CONN_VALID) { unlock(&conn->ownership); obj_put(conn); return -EINVAL; } } else { obj_put(conn); r = check_if_exiting(); return r; } if ((r = lock_ipc_handler_thread(conn)) != 0 ) goto out_obj_put; for (int i = 0 ; i < MAX_CAP_TRANSFER; i++) { conn->server_cap_buf[i].valid = false ; } ipc_thread_migrate_to_server( conn, conn->shm.server_shm_uaddr, conn->shm.shm_size, cap_num ); BUG("should not reach here\n" ); out_obj_put: unlock(&conn->ownership); obj_put(conn); return r; }
整体上就是由 cap 找到具体的 conn 对象,随后以此发起线程迁移(LRPC 中的技术),可参考 Lab 文档
该系统调用将设置服务器端的服务线程的栈地址、入口地址、各个参数,然后迁移到该服务器端服务线程继续运行。由于当前的客户端线程需要等待服务器端的服务线程处理完毕,因此需要更新其状态为 TS_WAITING,且不要加入等待队列
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 static void ipc_thread_migrate_to_server ( struct ipc_connection *conn, unsigned long shm_addr, size_t shm_size, unsigned int cap_num) { struct thread *target = conn->server_handler_thread; struct ipc_server_handler_config *handler_config = (struct ipc_server_handler_config *)target->general_ipc_config; handler_config->active_conn = conn; conn->current_client_thread = current_thread; thread_set_ts_blocking(current_thread); target->thread_ctx->sc = current_thread->thread_ctx->sc; arch_set_thread_stack(target, handler_config->ipc_routine_stack); arch_set_thread_next_ip(target, handler_config->ipc_routine_entry); arch_set_thread_arg0(target, shm_addr); arch_set_thread_arg1(target, shm_size); arch_set_thread_arg2(target, cap_num);#ifdef CHCORE_OPENTRUSTEE arch_set_thread_arg3( target, pid_to_taskid(current_thread->cap, conn->client_badge));#else arch_set_thread_arg3(target, conn->client_badge);#endif set_thread_arch_spec_state_ipc(target); sched_to_thread(target); BUG_ON(1 ); }
总结一下干了四件事:
更新状态为 TS_WAITING
设置 conn 为 active(避免并发问题)
设置 A-stack,寄存器
调用 sched_to_thread
切换控制流
切换到服务线程之后便会执行我们的 server_handler
,即 LRPC 的线程迁移技术中提到的“把代码拉过来执行”中被执行的部分,随后就是 server 端的处理操作了
IPC Return 最后,IPC 的服务端在操作完成后会使用 ipc_return
返回,毕竟这时候还是在 server 中
ipc_return
会发起 sys_ipc_return
系统调用,该系统调用会迁移回到 IPC 客户端线程继续运行,IPC 客户端线程从 ipc_call
中返回
也就是正常 return
需要替换成 ipc_return
在 server_handler
之中,根据 req 的信息完成 dispatch 的工作
1 2 3 4 5 6 7 8 9 10 11 12 DEFINE_SERVER_HANDLER(fsm_dispatch) { int ret = 0 ; struct fsm_request *fsm_req ; bool ret_with_cap = false ; if (ipc_msg == NULL ) { ipc_return(ipc_msg, -EINVAL); } }
1 2 3 4 5 6 7 8 9 void ipc_return (ipc_msg_t *ipc_msg, long ret) { if (ipc_msg != NULL ) { ipc_set_msg_return_cap_num(ipc_msg, 0 ); } usys_ipc_return((unsigned long )ret, 0 ); }
层层抽丝剥茧,来到我们最后的 sys_ipc_return
,这个 syscall 的源码很长,但是主要是在处理 edge case。概括的说它主要干了:
如果 server 线程退出,需要回收资源,并设置错误码
如果 client 线程退出,需要区分是普通线程还是影子线程(例如 client 线程也是 ipc 调用的 server, 即链式 ipc 调用),普通线程应该立刻回收,触发调度(做正常 return 的工作),影子线程的话就顺着调用链条,只做控制转移,让上层回收
能力等其他资源的清理
最后简单 thread_migrate_to_client(client, ret);
交换控制权,切换线程,完成整个调用,将 ret 值传回
带注释的源码如下,感兴趣者可作学习参考:
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 int sys_ipc_return (unsigned long ret, unsigned int cap_num) { struct ipc_server_handler_config *handler_config ; struct ipc_connection *conn ; struct thread *client ; handler_config = (struct ipc_server_handler_config *) current_thread->general_ipc_config; conn = handler_config->active_conn; if (!conn) return -EINVAL; client = conn->current_client_thread; if (thread_is_exiting(current_thread)) { kdebug("%s:%d Step-1\n" , __func__, __LINE__); conn->state = CONN_INCOME_STOPPED; thread_set_exited(current_thread); ret = -ESRCH; } if (thread_is_exiting(client)) { kdebug("%s:%d Step-2\n" , __func__, __LINE__); conn->state = CONN_INCOME_STOPPED; if (client->thread_ctx->type != TYPE_SHADOW) { kdebug("%s:%d Step-2.0\n" , __func__, __LINE__); handler_config->active_conn = NULL ; thread_set_ts_waiting(current_thread); current_thread->thread_ctx->sc = NULL ; unlock(&handler_config->ipc_lock); unlock(&conn->ownership); obj_put(conn); thread_set_exited(client); sched(); eret_to_thread(switch_context()); } } if (cap_num != 0 ) { for (int i = 0 ; i < MAX_CAP_TRANSFER; i++) { conn->client_cap_buf[i].valid = false ; } int r = ipc_send_cap(current_cap_group, conn->current_client_thread->cap_group, conn->server_cap_buf, conn->client_cap_buf, 0 , cap_num); if (r < 0 ) return r; } handler_config->active_conn = NULL ; thread_set_ts_waiting(current_thread); current_thread->thread_ctx->sc = NULL ; unlock(&handler_config->ipc_lock); unlock(&conn->ownership); obj_put(conn); thread_migrate_to_client(client, ret); BUG("should not reach here\n" ); __builtin_unreachable(); }
这样便成功返回了 Client 进程,继续做它该做的事情
逻辑流程图 至此,IPC 部分的源码解析到此结束,希望能对你的学习有所帮助