ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

PHASE 1

PHASE 1 PHASE 1一.Fork和exec和waitpid()总代码#includecerrno#includecstdio#includesys/wait.h#includeunistd.hintmain(){// 先准备参数避免 fork 后在 child 里做复杂 C 工作。charprogram[]/bin/echo;charmessage[]hello from child;char*argv[]{program,message,nullptr};constpid_t child::fork();if(child0){std::perror(fork);return1;}if(child0){// exec 成功不会返回只有失败才会执行下一行。::execv(program,argv);::_exit(127);}intstatus0;pid_t waited;do{waited::waitpid(child,status,0);}while(waited0errnoEINTR);if(waited0){std::perror(waitpid);return1;}if(WIFEXITED(status)){std::printf(exit code %d\n,WEXITSTATUS(status));returnWEXITSTATUS(status);}return1;}1.Fork的使用#includeiostream#includeunistd.hintmain(){std::coutfork 之前\n;fork();std::coutfork 之后\n;return0;}运行后fork 之前fork 之后fork 之后因为fork 前一个进程执行代码fork 后两个进程继续执行后面的代码constpid_t child::fork();if(child0){std::perror(fork);return1;}if(child0){// exec 成功不会返回只有失败才会执行下一行。::execv(program,argv);::_exit(127);}pid_t 现在可以简单理解成用来装 PID 这一类进程编号的整数类型。而result父子进程里的值不一样。子进程子进程看到result 0所以if(result0){// 我是 child}父进程父进程看到result0result 0而且这个正数就是child 的 PID2.父进程与子进程#includeiostream#includeunistd.hintmain(){std::coutfork 之前我的 PID getpid()\n;pid_t resultfork();if(result0){std::coutfork 失败\n;return1;}if(result0){std::cout我是 child我的 PID getpid()\n;}else{std::cout我是 parent我的 PID getpid()我创建的 child PID result\n;}return0;}输出fork 之前我的 PID 5000我是 parent我的 PID 5000我创建的 child PID 5001我是 child我的 PID 50013.exec的含义把“当前这个进程正在运行的程序”替换成另一个程序。charprogram[]/bin/echo;char*argv[]{program,message,nullptr};//nullptr表示参数到这里结束了...execv(program,argv);program是专门当路径用的字符串execv 会把 argv 里的内容交给 echo 程序echo 拿到之后会把它当成自己的参数来用。​注意std::coutbefore exec\n;execv(program,argv);std::coutafter exec\n;如果 execv() 成功before exec会执行但是after exec不会执行因为成功 exec 以后原来的程序已经被替换了。原程序后面的std::cout after exec\n;已经不属于当前运行中的程序了如果 execv() 成功执行路径实际上是child↓execv()↓原来的程序映像被 /bin/echo 替换↓开始执行 /bin/echo所以exec 成功不会返回。如果返回则失败并且exec成功pid不变4.waitpidwaitpid等待指定的 child并取得它的结束状态。fork 不会自动让 parent 等 child。parent 和 child 是两个独立进程。所以可能出现parent: TaskForge finishedchild : hello也可能child : helloparent: TaskForge finished谁先运行由操作系统调度决定。如果 parent 创建 child 后就不管了那 TaskForge 就没办法生成可靠的ProcessResult所以 parent 必须有办法等待 child 的状态变化并取得它的结束结果。这个系统调用就是waitpid()intstatus0;...waited::waitpid(child,status,0);status 是 Linux 返回的一份进程结束状态信息里面可能包含正常退出了吗退出码是多少还是被 signal 杀掉了被哪个 signal 杀掉status 是编码后的状态不能直接当退出码使用。用WIFEXITED(status)判断是否正常退出用WEXITSTATUS(status)得到真正退出码do{waited::waitpid(child,status,0);}while(waited0errnoEINTR);////EINTR突然 Linux 中间处理了某个 signal。于是这个 waitpid() 可能暂时返回失败但这种失败不一定意味着child 有问题。可能只是“刚才等的时候被打断了一下。”这种情况errno EINTR先等 child。如果 waitpid 失败并且失败原因只是 EINTR那就再等一次。一直重试直到成功或者出现真正的其他错误。二、Pipe总代码#includecerrno#includecstdio#includeunistd.hintmain(){intends[2];if(::pipe(ends)0){std::perror(pipe);return1;}constchartext[]abc;std::size_t sent0;boolfailedfalse;while(sent3){constssize_t n::write(ends[1],textsent,3-sent);if(n0)sentstatic_caststd::size_t(n);elseif(n0errnoEINTR)continue;else{failedtrue;break;}}::close(ends[1]);// 没有写端持有者了缓冲读完后才能读到 EOF。if(failed){::close(ends[0]);return1;}charbuffer[8];for(;;){constssize_t n::read(ends[0],buffer,sizeof(buffer));if(n0)std::printf(read %zd bytes: %.*s\n,n,static_castint(n),buffer);elseif(n0){std::puts(EOF);break;}elseif(errnoEINTR)continue;else{std::perror(read);::close(ends[0]);return1;}}::close(ends[0]);return0;}1.pipe概念输出要返回给调用方不能只显示在终端。文档这里的出发点很简单上一节 child 已经会通过 exec 去运行目标程序了但它输出的内容目前只是显示在终端TaskForge 自己还没有把这些内容收进 ProcessResult所以需要一条 child → parent 的数据通道这就是 pipe管道。注意是收集起来返回原来/bin/echo │ │stdout也就是 FD1▼ 终端 TaskForge 想要的/bin/echo │ ▼ pipe │ ▼ TaskForge parent2.第一段代码拆分intends[2];if(::pipe(ends)0){std::perror(pipe);return1;}::pipe(ends); //Linux 成功创建 pipe 后会把两个当前没被占用的文件描述符编号 填进这个数组pipe() 自己的返回值主要表示0 → 创建成功 0 → 创建失败ends[0]固定为读端ends[1]固定为写端3.第二段代码拆分(write)constssize_t n::write(ends[1],textsent,3-sent);write(写到哪里, 从哪里拿数据, 要写多少字节);ends[1]↓ 找到 pipe 的写端 ↓ 把abc写进 pipewrite这样写的原因第一次 write 想写 abc 实际写 ab 返回2sent2第二次 write 从 c 开始 还剩1字节 实际写 c 返回1sent3结束循环read的问题read() 返回 0不是说“这一次恰好没读到东西。”而是这个输入流已经结束了。如果只是暂时没数据但是写端还开着普通阻塞式 read() 通常会等而不是返回EOF情况1 pipe 里有数据 →read()读到数据 → 返回0情况2 pipe 里没数据 但写端还开着 →read()等待 情况3 pipe 里没数据 而且所有写端都关了 →read()返回0→EOF4.第三段代码拆分readcharbuffer[8];for(;;){constssize_t n::read(ends[0],buffer,sizeof(buffer));if(n0)std::printf(read %zd bytes: %.*s\n,n,static_castint(n),buffer);elseif(n0){std::puts(EOF);break;}elseif(errnoEINTR)continue;else{std::perror(read);::close(ends[0]);return1;}}其中read(ends[0],buffer,sizeof(buffer));表示从 pipe 的读端读取数据放进 buffer最多读取 sizeof(buffer) 个字节。三.dup2() 复制文件描述符到指定编号概念让 FD 1 不再指向终端而改成指向 pipe 的写端。流程图看pipe概念那一栏假设Linux 创建 pipe 后ends[0] 3 // 读端ends[1] 4 // 写端而child 里执行dup2(ends[1],STDOUT_FILENO);而STDOUT_FILENO就是1所以可以先理解成dup2(4,1)意思是让 FD 1 现在也指向 FD 4 所指向的那个资源。于是原来 FD1─────→ 终端 FD4─────→ pipe 写端 执行dup2(4,1);之后 FD1──┐ ├────→ pipe 写端 FD4──┘为什么 parent 和 child 都要关闭不需要的 pipe 端点fork() 后parent 和 child 都会继承 pipe 的读端和写端所以刚fork完大致是parent:FD3→ pipe 读端 FD4→ pipe 写端 child:FD3→ pipe 读端 FD4→ pipe 写端pipe 创建的这两个 fd3 和 4在 fork 前就已经在父进程里了但实际需要的职责是parent保留读端关闭写端 child 关闭读端保留写端原因1. 职责明确child 负责写输出parent 负责读取输出。2. 保证 EOF 正常出现只有当 pipe 为空且所有写端都关闭时read() 才返回 0EOF。如果 parent 忘记关闭自己的写端即使 child 已退出parent 也可能一直等不到 EOF。CLOEXEC为什么需要exec() 会替换程序但是不会默认关掉原有的fd当一直拿着fd时可能就会认为还有一个人拿着写端没关 → 读端 read 就认为还可能有人写→ 永远不返回 EOF → 父进程卡死CLOEXEC 用来防止 TaskForge 的内部 FD 泄漏到 exec 后的 workload但重定向后的 stdout/stderr也就是 FD 1、FD 2必须继续保留。FD 1 / FD 2是 workload 正常输出所依赖的所以 exec 后必须保留。四.RAII代码部分classUniqueFd{//现在造一个盒子把 fd 装进去,fd 归这个盒子管盒子死的时候帮你关public:explicitUniqueFd(intfd-1)noexcept:fd_(fd){}//它把传进来的 fd存进盒子内部的 fd_。~UniqueFd(){reset();}};情景intfdopen(...);if(something_failed){return1;}close(fd);如果中途步骤失败直接return那么close(fd)不会执行导致资源泄漏所以用UniqueFd来记录函数正常结束 UniqueFd 被销毁 ↓ 自动close(5)中途return UniqueFd 也会被销毁 ↓ 仍然自动close(5)例子执行 return 时为什么 FD 5 仍然有机会被自动关闭voidtest(){UniqueFdfd(5);if(true){return;}}答return 让 test() 的局部作用域结束局部对象 fd 随之析构自动调用 ~UniqueFd()而析构函数内部再负责关闭它拥有的 FD 5。提醒UniqueFd 禁止复制是为了避免同一个 FD 被多个对象共同“拥有”导致重复 close。也就是要避免这种UniqueFda(5);UniqueFd ba;因为这种会有double close重复关闭的风险这会让两个对象都认为自己拥有 FD 5。但可以设计成UniqueFd bstd::move(a);//把 FD 5 的所有权从 a 转交给 b。转移之后应该是a不再拥有有效 FD b拥有 FD5五.waitpid()和读日志的顺序不能先 waitpid() 再读日志因为比如一个child无限多日志所以可能出现child 不停 write 日志 ↓ pipe 越来越满 ↓ pipe 满了 ↓ child 的 write 卡住等 parent 来读与此同时parent 正在waitpid()↓ 等 child 退出程序卡住正确思路child 运行期间parent 就要持续把 pipe 里的数据读走。drainchild 继续写 ↓ pipe 有数据 ↓ parent 持续 read ↓ pipe 腾出空间 ↓ child 可以继续 write六.阻塞式 read()的解决方案POLLpoll 等待文件描述符事件可以同时等待 stdout、stderr、startup 等多个 FD 的状态变化。作用负责真正把数据读出来。Q为什么 TaskForge 不能简单地依次对 stdout、stderr、startup 三个 pipe 做阻塞式 read()A如果按 stdout → stderr → startup 的顺序做阻塞式 read()而 stdout 此刻没数据parent 可能先卡在 stdout 上。即使 stderr 已经有很多数据也没机会去读stderr 管道甚至可能继续被 child 写满。但poll 不只是看一眼就立刻返回。它还有等待的语义。而等多久取决于你给它设的超时参数。超时参数 行为 无限 没有任何 pipe 就绪就永远等不返回0立刻返回看一眼就走不等 某个正数如20ms 最多等这么久要么有 pipe 就绪要么时间到就返回nonblocking I/O如果把 pipe 的读端设成O_NONBLOCK那么当暂时没有数据时read(…)不会一直卡住而是马上返回失败并通过errno EAGAIN或者errno EWOULDBLOCK告诉你“现在暂时没数据不是 pipe 坏了你过一会儿再来。”child 不断输出 ↓stdout/stderrpipe ↓ parent 用 poll 看哪些 FD 有事件 ↓ 对有数据的 FD 做 nonblocking read ↓ 持续 drain给 pipe 腾空间非阻塞 read 遇到“暂时无数据”不会等待而是返回 EAGAIN/EWOULDBLOCK只有输入真正结束时才返回 0 表示 EOF。区别poll→ 告诉你哪个 FD 值得处理nonblocking read→ 真正读取但不会因为暂时没数据把整个 parent 卡死七.启动失败必须有自己的通道startup pipe启动错误管道专门传child 在 exec 前阶段发生的启动错误如果 child 在设置 FDexec这些启动阶段出错就往 startup pipe 里写错误信息。例如阶段exec 错误编号ENOENTparent 就知道这不是 workload 正常退出而是根本没启动成功。如果 exec 成功startup pipe 的 child 写端设置CLOEXEC所以exec 成功 ↓ startup pipe 写端自动关闭parent 就能观察到startup pipe 没收到错误消息 并且写端关闭但不能仅凭 startup pipe 的“无消息 EOF”就断言 exec 一定成功。因为还可能存在child 在来得及报告错误之前 ↓ 被 signal 杀掉 这种情况。八.多线程环境里 fork() 之后的风险fork后复制的是一整个进程不是线程——包括整份内存、所有 FD、各种状态。fork() 后 child 得到 parent 当时进程内存状态的一份逻辑副本但只保留调用 fork() 的那条线程。假设TaskForge 此时已经是一个多线程程序线程 A 线程 B 线程 C假设fork 之前线程 B 干了这件事线程 B把 mutex 锁上了 ← 但还没解锁现在线程 A 调用了fork();此时parent 线程 A、线程 B、线程 C 全都在 child 线程 A 只有 Afork 会复制 parent 的整个内存给 child。内存里那个 mutex 的状态是已锁于是 child 里child 的内存里 mutex 状态已锁 原样抄过来的 child 的线程里 只有 A没有 B 开锁的人没了最后导致程序卡死九.为什么不用epollpoll → 少量 FD 时简单直接 epoll → 大量 FD、统一事件循环时更有优势当前每个任务只需要监听少量 stdout、stderr、startup 等 FDpoll 的结构足够直接epoll 更适合大量 FD 集中在单个事件循环中的场景因此在这里换成 epoll 不一定带来实际收益。QQ为什么 execv() 失败以后child 这里更倾向于用 _exit()而不是普通的 exit()Achild 出错时使用 _exit()是为了直接退出进程不执行普通 exit() 会做的那些用户态清理、退出处理函数和 stdio 刷新QCLOEXEC 和 close() 有什么区别Aclose(fd)→ 现在立即关闭CLOEXEC→ 现在继续保留→ exec 成功时自动关闭
返回列表