shim_ipc.c 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599
  1. /* Copyright (C) 2014 Stony Brook University
  2. This file is part of Graphene Library OS.
  3. Graphene Library OS is free software: you can redistribute it and/or
  4. modify it under the terms of the GNU Lesser General Public License
  5. as published by the Free Software Foundation, either version 3 of the
  6. License, or (at your option) any later version.
  7. Graphene Library OS is distributed in the hope that it will be useful,
  8. but WITHOUT ANY WARRANTY; without even the implied warranty of
  9. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  10. GNU Lesser General Public License for more details.
  11. You should have received a copy of the GNU Lesser General Public License
  12. along with this program. If not, see <http://www.gnu.org/licenses/>. */
  13. /*
  14. * shim_ipc.c
  15. *
  16. * This file contains code to maintain generic bookkeeping of IPC: operations
  17. * on shim_ipc_msg (one-way IPC messages), shim_ipc_msg_duplex (IPC messages
  18. * with acknowledgement), shim_ipc_info (IPC ports of process), shim_process.
  19. */
  20. #include <list.h>
  21. #include <pal.h>
  22. #include <pal_error.h>
  23. #include <shim_checkpoint.h>
  24. #include <shim_handle.h>
  25. #include <shim_internal.h>
  26. #include <shim_ipc.h>
  27. #include <shim_profile.h>
  28. #include <shim_thread.h>
  29. #include <shim_unistd.h>
  30. #include <shim_utils.h>
  31. #define IPC_INFO_MGR_ALLOC 32
  32. #define PAGE_SIZE allocsize
  33. #define OBJ_TYPE struct shim_ipc_info
  34. #include "memmgr.h"
  35. static MEM_MGR ipc_info_mgr;
  36. struct shim_lock ipc_info_lock;
  37. struct shim_process cur_process;
  38. #define CLIENT_HASH_BITLEN 6
  39. #define CLIENT_HASH_NUM (1 << CLIENT_HASH_BITLEN)
  40. #define CLIENT_HASH_MASK (CLIENT_HASH_NUM - 1)
  41. #define CLIENT_HASH(vmid) ((vmid)&CLIENT_HASH_MASK)
  42. DEFINE_LISTP(shim_ipc_info);
  43. static LISTP_TYPE(shim_ipc_info) info_hlist[CLIENT_HASH_NUM];
  44. DEFINE_PROFILE_CATEGORY(ipc, );
  45. DEFINE_PROFILE_OCCURENCE(syscall_use_ipc, ipc);
  46. int init_ipc_ports(void);
  47. int init_ns_pid(void);
  48. int init_ns_sysv(void);
  49. int init_ipc(void) {
  50. int ret = 0;
  51. create_lock(&ipc_info_lock);
  52. if (!(ipc_info_mgr = create_mem_mgr(init_align_up(IPC_INFO_MGR_ALLOC))))
  53. return -ENOMEM;
  54. if ((ret = init_ipc_ports()) < 0)
  55. return ret;
  56. if ((ret = init_ns_pid()) < 0)
  57. return ret;
  58. if ((ret = init_ns_sysv()) < 0)
  59. return ret;
  60. return 0;
  61. }
  62. int prepare_ns_leaders(void) {
  63. int ret = 0;
  64. if ((ret = prepare_pid_leader()) < 0)
  65. return ret;
  66. if ((ret = prepare_sysv_leader()) < 0)
  67. return ret;
  68. return 0;
  69. }
  70. static struct shim_ipc_info* __create_ipc_info(IDTYPE vmid, const char* uri, size_t len) {
  71. struct shim_ipc_info* info =
  72. get_mem_obj_from_mgr_enlarge(ipc_info_mgr, size_align_up(IPC_INFO_MGR_ALLOC));
  73. if (!info)
  74. return NULL;
  75. memset(info, 0, sizeof(struct shim_ipc_info));
  76. info->vmid = vmid;
  77. if (uri)
  78. qstrsetstr(&info->uri, uri, len);
  79. REF_SET(info->ref_count, 1);
  80. INIT_LIST_HEAD(info, hlist);
  81. return info;
  82. }
  83. static void __free_ipc_info(struct shim_ipc_info* info) {
  84. if (info->pal_handle) {
  85. DkObjectClose(info->pal_handle);
  86. info->pal_handle = NULL;
  87. }
  88. if (info->port)
  89. put_ipc_port(info->port);
  90. qstrfree(&info->uri);
  91. free_mem_obj_to_mgr(ipc_info_mgr, info);
  92. }
  93. static void __get_ipc_info(struct shim_ipc_info* info) {
  94. REF_INC(info->ref_count);
  95. }
  96. static void __put_ipc_info(struct shim_ipc_info* info) {
  97. int ref_count = REF_DEC(info->ref_count);
  98. if (!ref_count)
  99. __free_ipc_info(info);
  100. }
  101. void get_ipc_info(struct shim_ipc_info* info) {
  102. /* no need to grab ipc_info_lock because __get_ipc_info() is atomic */
  103. __get_ipc_info(info);
  104. }
  105. void put_ipc_info(struct shim_ipc_info* info) {
  106. /* this is atomic so we don't grab lock in common case of ref_count > 0 */
  107. int ref_count = REF_DEC(info->ref_count);
  108. if (!ref_count) {
  109. lock(&ipc_info_lock);
  110. __free_ipc_info(info);
  111. unlock(&ipc_info_lock);
  112. }
  113. }
  114. struct shim_ipc_info* create_ipc_info(IDTYPE vmid, const char* uri, size_t len) {
  115. lock(&ipc_info_lock);
  116. struct shim_ipc_info* info = __create_ipc_info(vmid, uri, len);
  117. unlock(&ipc_info_lock);
  118. return info;
  119. }
  120. struct shim_ipc_info* create_ipc_info_in_list(IDTYPE vmid, const char* uri, size_t len) {
  121. assert(vmid);
  122. struct shim_ipc_info* info;
  123. lock(&ipc_info_lock);
  124. /* check if info with this vmid & uri already exists and return it */
  125. LISTP_TYPE(shim_ipc_info)* info_bucket = &info_hlist[CLIENT_HASH(vmid)];
  126. LISTP_FOR_EACH_ENTRY(info, info_bucket, hlist) {
  127. if (info->vmid == vmid && !qstrcmpstr(&info->uri, uri, len)) {
  128. get_ipc_info(info);
  129. unlock(&ipc_info_lock);
  130. return info;
  131. }
  132. }
  133. /* otherwise create new info and return it */
  134. info = __create_ipc_info(vmid, uri, len);
  135. if (info) {
  136. LISTP_ADD(info, info_bucket, hlist);
  137. get_ipc_info(info);
  138. }
  139. unlock(&ipc_info_lock);
  140. return info;
  141. }
  142. void put_ipc_info_in_list(struct shim_ipc_info* info) {
  143. LISTP_TYPE(shim_ipc_info)* info_bucket = &info_hlist[CLIENT_HASH(info->vmid)];
  144. lock(&ipc_info_lock);
  145. __put_ipc_info(info);
  146. if (REF_GET(info->ref_count) == 1) {
  147. LISTP_DEL_INIT(info, info_bucket, hlist);
  148. __put_ipc_info(info);
  149. }
  150. unlock(&ipc_info_lock);
  151. }
  152. struct shim_ipc_info* lookup_ipc_info(IDTYPE vmid) {
  153. assert(vmid);
  154. lock(&ipc_info_lock);
  155. struct shim_ipc_info* info;
  156. LISTP_TYPE(shim_ipc_info)* info_bucket = &info_hlist[CLIENT_HASH(vmid)];
  157. LISTP_FOR_EACH_ENTRY(info, info_bucket, hlist) {
  158. if (info->vmid == vmid && !qstrempty(&info->uri)) {
  159. __get_ipc_info(info);
  160. unlock(&ipc_info_lock);
  161. return info;
  162. }
  163. }
  164. unlock(&ipc_info_lock);
  165. return NULL;
  166. }
  167. struct shim_process* create_process(bool dup_cur_process) {
  168. struct shim_process* new_process = calloc(1, sizeof(struct shim_process));
  169. if (!new_process)
  170. return NULL;
  171. lock(&cur_process.lock);
  172. /* current process must have been initialized with info on its own IPC info */
  173. assert(cur_process.self);
  174. assert(cur_process.self->pal_handle && !qstrempty(&cur_process.self->uri));
  175. if (dup_cur_process) {
  176. /* execve case, new process assumes identity of current process and thus has
  177. * - same vmid as current process
  178. * - same self IPC info as current process
  179. * - same parent IPC info as current process
  180. */
  181. new_process->vmid = cur_process.vmid;
  182. new_process->self = create_ipc_info(
  183. cur_process.self->vmid, qstrgetstr(&cur_process.self->uri), cur_process.self->uri.len);
  184. new_process->self->pal_handle = cur_process.self->pal_handle;
  185. if (!new_process->self) {
  186. unlock(&cur_process.lock);
  187. return NULL;
  188. }
  189. /* there is a corner case of execve in very first process; such process does
  190. * not have parent process, so cannot copy parent IPC info */
  191. if (cur_process.parent) {
  192. new_process->parent =
  193. create_ipc_info(cur_process.parent->vmid, qstrgetstr(&cur_process.parent->uri),
  194. cur_process.parent->uri.len);
  195. new_process->parent->pal_handle = cur_process.parent->pal_handle;
  196. }
  197. } else {
  198. /* fork/clone case, new process has new identity but inherits parent */
  199. new_process->vmid = 0;
  200. new_process->self = NULL;
  201. new_process->parent = create_ipc_info(
  202. cur_process.self->vmid, qstrgetstr(&cur_process.self->uri), cur_process.self->uri.len);
  203. }
  204. if (cur_process.parent && !new_process->parent) {
  205. if (new_process->self)
  206. put_ipc_info(new_process->self);
  207. unlock(&cur_process.lock);
  208. return NULL;
  209. }
  210. /* new process inherits the same namespace leaders */
  211. for (int i = 0; i < TOTAL_NS; i++) {
  212. if (cur_process.ns[i]) {
  213. new_process->ns[i] =
  214. create_ipc_info(cur_process.ns[i]->vmid, qstrgetstr(&cur_process.ns[i]->uri),
  215. cur_process.ns[i]->uri.len);
  216. if (!new_process->ns[i]) {
  217. if (new_process->self)
  218. put_ipc_info(new_process->self);
  219. if (new_process->parent)
  220. put_ipc_info(new_process->parent);
  221. for (int j = 0; j < i; j++) {
  222. put_ipc_info(new_process->ns[j]);
  223. }
  224. unlock(&cur_process.lock);
  225. return NULL;
  226. }
  227. }
  228. }
  229. unlock(&cur_process.lock);
  230. return new_process;
  231. }
  232. void free_process(struct shim_process* process) {
  233. if (process->self)
  234. put_ipc_info(process->self);
  235. if (process->parent)
  236. put_ipc_info(process->parent);
  237. for (int i = 0; i < TOTAL_NS; i++)
  238. if (process->ns[i])
  239. put_ipc_info(process->ns[i]);
  240. free(process);
  241. }
  242. void init_ipc_msg(struct shim_ipc_msg* msg, int code, size_t size, IDTYPE dest) {
  243. msg->code = code;
  244. msg->size = get_ipc_msg_size(size);
  245. msg->src = cur_process.vmid;
  246. msg->dst = dest;
  247. msg->seq = 0;
  248. }
  249. void init_ipc_msg_duplex(struct shim_ipc_msg_duplex* msg, int code, size_t size, IDTYPE dest) {
  250. init_ipc_msg(&msg->msg, code, size, dest);
  251. msg->thread = NULL;
  252. INIT_LIST_HEAD(msg, list);
  253. msg->retval = 0;
  254. msg->private = NULL;
  255. }
  256. int send_ipc_message(struct shim_ipc_msg* msg, struct shim_ipc_port* port) {
  257. assert(msg->size >= IPC_MSG_MINIMAL_SIZE);
  258. msg->src = cur_process.vmid;
  259. debug("Sending ipc message to port %p (handle %p)\n", port, port->pal_handle);
  260. size_t total_bytes = msg->size;
  261. size_t bytes = 0;
  262. do {
  263. size_t ret =
  264. DkStreamWrite(port->pal_handle, 0, total_bytes - bytes, (void*)msg + bytes, NULL);
  265. if (!ret) {
  266. if (PAL_ERRNO == EINTR || PAL_ERRNO == EAGAIN || PAL_ERRNO == EWOULDBLOCK)
  267. continue;
  268. debug("Port %p (handle %p) was removed during sending\n", port, port->pal_handle);
  269. del_ipc_port_fini(port, -ECHILD);
  270. return -PAL_ERRNO;
  271. }
  272. bytes += ret;
  273. } while (bytes < total_bytes);
  274. return 0;
  275. }
  276. struct shim_ipc_msg_duplex* pop_ipc_msg_duplex(struct shim_ipc_port* port, unsigned long seq) {
  277. struct shim_ipc_msg_duplex* found = NULL;
  278. lock(&port->msgs_lock);
  279. struct shim_ipc_msg_duplex* tmp;
  280. LISTP_FOR_EACH_ENTRY(tmp, &port->msgs, list) {
  281. if (tmp->msg.seq == seq) {
  282. found = tmp;
  283. LISTP_DEL_INIT(tmp, &port->msgs, list);
  284. break;
  285. }
  286. }
  287. unlock(&port->msgs_lock);
  288. return found;
  289. }
  290. int send_ipc_message_duplex(struct shim_ipc_msg_duplex* msg, struct shim_ipc_port* port,
  291. unsigned long* seq, void* private_data) {
  292. int ret = 0;
  293. struct shim_thread* thread = get_cur_thread();
  294. assert(thread);
  295. /* prepare thread which will send the message for waiting for response
  296. * (this also acquires reference to the thread) */
  297. if (!msg->thread)
  298. thread_setwait(&msg->thread, thread);
  299. static struct atomic_int ipc_seq_counter;
  300. msg->msg.seq = atomic_inc_return(&ipc_seq_counter);
  301. /* save the message to list of port msgs together with its private data */
  302. lock(&port->msgs_lock);
  303. msg->private = private_data;
  304. LISTP_ADD_TAIL(msg, &port->msgs, list);
  305. unlock(&port->msgs_lock);
  306. ret = send_ipc_message(&msg->msg, port);
  307. if (ret < 0)
  308. goto out;
  309. if (seq)
  310. *seq = msg->msg.seq;
  311. debug("Waiting for response (seq = %lu)\n", msg->msg.seq);
  312. /* force thread which will send the message to wait for response;
  313. * ignore unrelated interrupts but fail on actual errors */
  314. do {
  315. ret = thread_sleep(NO_TIMEOUT);
  316. if (ret < 0 && ret != -EINTR && ret != -EAGAIN)
  317. goto out;
  318. } while (ret != 0);
  319. debug("Finished waiting for response (seq = %lu, ret = %d)\n", msg->msg.seq, msg->retval);
  320. ret = msg->retval;
  321. out:
  322. lock(&port->msgs_lock);
  323. if (!LIST_EMPTY(msg, list))
  324. LISTP_DEL_INIT(msg, &port->msgs, list);
  325. unlock(&port->msgs_lock);
  326. if (msg->thread) {
  327. /* put reference to the thread acquired earlier */
  328. put_thread(msg->thread);
  329. msg->thread = NULL;
  330. }
  331. return ret;
  332. }
  333. /* must be called with cur_process.lock taken */
  334. struct shim_ipc_info* create_ipc_info_cur_process(bool is_self_ipc_info) {
  335. struct shim_ipc_info* info = create_ipc_info(cur_process.vmid, NULL, 0);
  336. if (!info)
  337. return NULL;
  338. /* pipe for cur_process.self is of format "pipe:<cur_process.vmid>", others with random name */
  339. char uri[PIPE_URI_SIZE];
  340. if (create_pipe(NULL, uri, PIPE_URI_SIZE, &info->pal_handle, &info->uri, is_self_ipc_info) <
  341. 0) {
  342. put_ipc_info(info);
  343. return NULL;
  344. }
  345. add_ipc_port_by_id(cur_process.vmid, info->pal_handle, IPC_PORT_SERVER, NULL, &info->port);
  346. return info;
  347. }
  348. int get_ipc_info_cur_process(struct shim_ipc_info** info) {
  349. lock(&cur_process.lock);
  350. if (!cur_process.self) {
  351. cur_process.self = create_ipc_info_cur_process(true);
  352. if (!cur_process.self) {
  353. unlock(&cur_process.lock);
  354. return -EACCES;
  355. }
  356. }
  357. get_ipc_info(cur_process.self);
  358. *info = cur_process.self;
  359. unlock(&cur_process.lock);
  360. return 0;
  361. }
  362. DEFINE_PROFILE_INTERVAL(ipc_checkpoint_send, ipc);
  363. DEFINE_PROFILE_INTERVAL(ipc_checkpoint_callback, ipc);
  364. /* Graphene's checkpoint() syscall broadcasts a msg to all processes
  365. * asking to checkpoint their state and save in process-unique file in
  366. * directory cpdir under session cpsession. */
  367. int ipc_checkpoint_send(const char* cpdir, IDTYPE cpsession) {
  368. BEGIN_PROFILE_INTERVAL();
  369. int ret;
  370. size_t len = strlen(cpdir);
  371. size_t total_msg_size = get_ipc_msg_size(sizeof(struct shim_ipc_checkpoint) + len);
  372. struct shim_ipc_msg* msg = __alloca(total_msg_size);
  373. init_ipc_msg(msg, IPC_CHECKPOINT, total_msg_size, 0);
  374. struct shim_ipc_checkpoint* msgin = (struct shim_ipc_checkpoint*)&msg->msg;
  375. msgin->cpsession = cpsession;
  376. memcpy(&msgin->cpdir, cpdir, len + 1);
  377. debug("IPC broadcast to all: IPC_CHECKPOINT(%u, %s)\n", cpsession, cpdir);
  378. /* broadcast to all including myself (so I can also checkpoint) */
  379. ret = broadcast_ipc(msg, IPC_PORT_DIRCLD | IPC_PORT_DIRPRT,
  380. /*exclude_port=*/NULL);
  381. SAVE_PROFILE_INTERVAL(ipc_checkpoint_send);
  382. return ret;
  383. }
  384. /* This process is asked to create a checkpoint, so it:
  385. * - sends a Graphene-specific SIGCP signal to all its threads (for
  386. * all to stop and join the checkpoint for consistent state),
  387. * - broadcasts checkpoint msg further to other processes. */
  388. int ipc_checkpoint_callback(struct shim_ipc_msg* msg, struct shim_ipc_port* port) {
  389. BEGIN_PROFILE_INTERVAL();
  390. int ret = 0;
  391. struct shim_ipc_checkpoint* msgin = (struct shim_ipc_checkpoint*)msg->msg;
  392. debug("IPC callback from %u: IPC_CHECKPOINT(%u, %s)\n", msg->src, msgin->cpsession,
  393. msgin->cpdir);
  394. ret = create_checkpoint(msgin->cpdir, &msgin->cpsession);
  395. if (ret < 0)
  396. goto out;
  397. kill_all_threads(NULL, msgin->cpsession, SIGCP);
  398. broadcast_ipc(msg, IPC_PORT_DIRCLD | IPC_PORT_DIRPRT, port);
  399. out:
  400. SAVE_PROFILE_INTERVAL(ipc_checkpoint_callback);
  401. return ret;
  402. }
  403. BEGIN_CP_FUNC(ipc_info) {
  404. assert(size == sizeof(struct shim_ipc_info));
  405. struct shim_ipc_info* info = (struct shim_ipc_info*)obj;
  406. struct shim_ipc_info* new_info = NULL;
  407. ptr_t off = GET_FROM_CP_MAP(obj);
  408. if (!off) {
  409. off = ADD_CP_OFFSET(sizeof(struct shim_ipc_info));
  410. ADD_TO_CP_MAP(obj, off);
  411. new_info = (struct shim_ipc_info*)(base + off);
  412. memcpy(new_info, info, sizeof(struct shim_ipc_info));
  413. REF_SET(new_info->ref_count, 0);
  414. /* call qstr-specific checkpointing function for new_info->uri */
  415. DO_CP_IN_MEMBER(qstr, new_info, uri);
  416. if (info->pal_handle) {
  417. struct shim_palhdl_entry* entry;
  418. /* call palhdl-specific checkpointing function to checkpoint
  419. * info->pal_handle and return created object in entry */
  420. DO_CP(palhdl, info->pal_handle, &entry);
  421. /* info's PAL handle will be re-opened with new URI during
  422. * palhdl restore (see checkpoint.c) */
  423. entry->uri = &new_info->uri;
  424. entry->phandle = &new_info->pal_handle;
  425. }
  426. } else {
  427. /* already checkpointed */
  428. new_info = (struct shim_ipc_info*)(base + off);
  429. }
  430. if (new_info && objp)
  431. *objp = (void*)new_info;
  432. }
  433. END_CP_FUNC_NO_RS(ipc_info)
  434. BEGIN_CP_FUNC(process) {
  435. assert(size == sizeof(struct shim_process));
  436. struct shim_process* process = (struct shim_process*)obj;
  437. struct shim_process* new_process = NULL;
  438. ptr_t off = GET_FROM_CP_MAP(obj);
  439. if (!off) {
  440. off = ADD_CP_OFFSET(sizeof(struct shim_process));
  441. ADD_TO_CP_MAP(obj, off);
  442. new_process = (struct shim_process*)(base + off);
  443. memcpy(new_process, process, sizeof(struct shim_process));
  444. /* call ipc_info-specific checkpointing functions
  445. * for new_process's self, parent, and ns infos */
  446. if (process->self)
  447. DO_CP_MEMBER(ipc_info, process, new_process, self);
  448. if (process->parent)
  449. DO_CP_MEMBER(ipc_info, process, new_process, parent);
  450. for (int i = 0; i < TOTAL_NS; i++)
  451. if (process->ns[i])
  452. DO_CP_MEMBER(ipc_info, process, new_process, ns[i]);
  453. ADD_CP_FUNC_ENTRY(off);
  454. } else {
  455. /* already checkpointed */
  456. new_process = (struct shim_process*)(base + off);
  457. }
  458. if (objp)
  459. *objp = (void*)new_process;
  460. }
  461. END_CP_FUNC(process)
  462. BEGIN_RS_FUNC(process) {
  463. __UNUSED(offset);
  464. struct shim_process* process = (void*)(base + GET_CP_FUNC_ENTRY());
  465. /* process vmid = 0: fork/clone case, forces to pick up new host-OS vmid
  466. * process vmid != 0: execve case, forces to re-use vmid of parent */
  467. if (!process->vmid)
  468. process->vmid = cur_process.vmid;
  469. CP_REBASE(process->self);
  470. CP_REBASE(process->parent);
  471. CP_REBASE(process->ns);
  472. if (process->self) {
  473. process->self->vmid = process->vmid;
  474. get_ipc_info(process->self);
  475. }
  476. if (process->parent)
  477. get_ipc_info(process->parent);
  478. for (int i = 0; i < TOTAL_NS; i++)
  479. if (process->ns[i])
  480. get_ipc_info(process->ns[i]);
  481. memcpy(&cur_process, process, sizeof(struct shim_process));
  482. create_lock(&cur_process.lock);
  483. DEBUG_RS("vmid=%u,uri=%s,parent=%u(%s)", process->vmid,
  484. process->self ? qstrgetstr(&process->self->uri) : "",
  485. process->parent ? process->parent->vmid : 0,
  486. process->parent ? qstrgetstr(&process->parent->uri) : "");
  487. }
  488. END_RS_FUNC(process)