news 2026/4/25 10:33:17

进程控制(上)---进程创建和进程终止

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
进程控制(上)---进程创建和进程终止

目录

1.进程创建

1.1fork函数

1-2 fork函数返回值

1-3 写时拷贝

2.

为什么要写时拷贝?

1-4 fork常规用法

1-5 fork调用失败的原因

2.进程终止

2-1 进程退出场景

2-2 进程常见退出方法

main对应的返回值给谁了呢?main函数的返回值代表什么含义呢?

退出码

_exit函数

exit

exit vs _exit

return退出


1.进程创建

1.1fork函数

之前我们也介绍过fork,看过fork函数可以跳过至写时拷贝标记处。

在 linux 中 fork 函数是非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程, 而原进程为父进程。

#include<unistd.h>

pid_t fork(void);

返回值:子进程中返回0,父进程返回子进程id,出错返回-1

1. 为什么要给子进程返回0,父进程返回子进程pid?

2. 为甚一个函数fork会有两个返回值?

3. 为什么一个id即等于0,又大于0?

进程调用 fork ,当控制转移到内核中的 fork 代码后,内核做:

• 分配新的内存块和内核数据结构给子进程

• 将父进程部分数据结构内容拷贝至子进程

• 添加子进程到系统进程列表当中

• fork 返回,开始调度器调度

以及之前父子进程

fork是一个系统调用,用于创建新进程

正常代码是一个执行流的,而当我们执行fork会变成两个执行流,这两个执行流都会执行后续的代码

[user1@iZ5waahoxw3q2bZ 26-4-20]$ cat myprocess.c #include<stdio.h> #include<unistd.h> #include<sys/types.h> int main() { printf("父进程开始运行,pid:%d\n",getpid()); fork(); printf("进程开始执行,pid:%d\n",getpid()); }

所以到时候下面那个printf被执行两次,而getpid打印出来的值应该是不一样的

[user1@iZ5waahoxw3q2bZ 26-4-20]$ ./myprocess 父进程开始运行,pid:19584 进程开始执行,pid:19584 进程开始执行,pid:19585

确实会创建两个进程

task_struct
父进程 代码 数据
-子进程子进程没有自己的代码和数据,因为目前,没有程序新加载!共享父进程的代码和数据
子进程复制了父进程的代码段、数据段、堆栈等

子进程会默认执行父进程的数据和代码,所以子进程被调度的时候,就会执行父进程之后的代码

fork之前父进程独立执行,fork之后,父子两个执行流分别执行。注意,fork之后,谁先执行完全由调度器决定。

1-2 fork函数返回值

fork返回值

RETURN VALUE On success, the PID of the child process is returned in the parent, and 0 is returned in the child. On failure, -1 is returned in the parent, no child process is created, and errno is set appropriately.

如果成功了,它的返回值会把子进程的pid返回给父进程,0返回给子进程,失败返回

在 fork() 调用时:
内核创建子进程,复制父进程的几乎所有资源
1.在父进程上下文中:返回 子进程的PID(> 0)
2.在子进程上下文中:返回 0

1-3 写时拷贝

通常,父子代码共享,父子再不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方 式各自一份副本。

父子进程都有自己对应的一套页表,虚拟地址空间

子进程这个task_struct是拷贝自父进程的,地址空间、页表同样也是。同样的在子进程内也应该有一个全局变量g_val,这就发生浅拷贝了。父子的代码以及其他数据同样也是如此,这样父子在默认情况下就共享了。

浅拷贝


当子进程的g_val++,那么内存会重新开辟一个空间,把老变量里面的内容拷贝进来,此时就得到一个新的物理地址。从而构建一个新的映射关系

子进程对变量进行修改,构建全新的映射关系,虚拟内存不变,物理内存指向一个新的物理地址---这种机制称之为写时拷贝。

2.

代码段是只读的,可是数据段怎么也是只读的?

其实在进程中,如果父进程没有创建子进程的话,代码段是只读的,数据段是读写的

一旦创建子进程,操作系统会把数据段的进程也改成只读的。

当子进程访问数据,操作系统检测到对一个只读的地方进行写入会"出/报错"。然后发现是数据段且是子进程,操作系统会触发写时拷贝。

  1. 触发条件:子进程(或父进程)尝试向一个被标记为“只读”的内存页进行写入操作。

  2. 异常捕获:CPU 的 MMU 检测到“写保护”违规,触发缺页异常,并报告异常类型为“写入只读页”。

  3. 内核处理:操作系统的缺页异常处理程序会检查:

    • 该虚拟地址是否在进程的合法内存区域内(如数据段、堆、栈)。

    • 该页是否为写时拷贝页(通常通过检查页表项的标志位,例如_PAGE_COW)。

    • 且确认是子进程:实际上,内核通过异常现场信息即可知道是哪个进程在访问。COW 机制对父子进程的行为是对称的,谁先写入,谁就触发 COW。你的表述“是子进程”在 fork 场景下是正确的,但如果父进程在 fork 后也先写入,同样会触发 COW。因此,更通用的说法是“是 fork 产生的相关进程”。

  4. 执行拷贝:确认满足条件后,内核会执行以下动作:

    • 分配一个新的物理内存页。

    • 将原物理页中的内容拷贝到新页中。

    • 触发异常的进程的页表中,将该虚拟地址映射到新的物理页,并恢复其读写权限

    • 在另一个进程的页表中,对应映射依然指向原来的物理页,并保持只读状态。

  5. 恢复执行:内核修改完成后,重新执行那条引发异常的写入指令。此时,写入操作的对象已经是该进程私有的物理副本了。可以看出,整个过程的核心是将共享资源的写入操作,透明地转换为私有资源的创建与写入。这是一种非常典型的“用时复制”思想,在数据段、堆、栈等所有可写区域都会生效。

为什么要写时拷贝?

1.减少创建时间

2.减少内存浪费

怕浪费空间,出现重复数据

因为有写时拷贝技术的存在,所以父子进程得以彻底分离!完成了进程独立性的技术保证! 写时拷贝,是一种延时申请技术,可以提高整机内存的使用率。让子进程只拷贝自己想改的,不想改的就不变

1-4 fork常规用法

一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如,父进程等待客户端请求, 生成子进程来处理请求。

一个进程要执行一个不同的程序。例如子进程从fork返回后,调用exec函数。

1-5 fork调用失败的原因

• 系统中有太多的进程

• 实际用户的进程数超过了限制

2.进程终止

进程终止的本质是释放系统资源,就是释放进程申请的相关内核数据结构和对应的数据和代码。

2-1 进程退出场景

子进程也是进程->父进程创建->子进程执行的结果

• 代码运行完毕,结果正确

• 代码运行完毕,结果不正确

• 代码异常终止

2-2 进程常见退出方法

正常终止(可以通过 echo $? 查看进程退出码):

1. 从main返回

2. 调用exit

3. _exit

异常退出:

• ctrl + c,信号终止

int main();

main对应的返回值给谁了呢?main函数的返回值代表什么含义呢?

main函数的返回值,通常表明你的程序的执行情况,通常是前两种

由程序的退出码决定!

• 代码运行完毕,结果正确 return 0

• 代码运行完毕,结果不正确 return {1,2,3,..非0}---不同的值,表明不同的出错原因!

• 代码异常终止

为什么我们比如在vs的时候有时候可以不写返回值呢?默认会帮你返回

eg

a() -> b()

| mov eax 1:ret

call b;

mov 0x123[] eax;

一般返回值是通过寄存器返回的

[user1@iZ5waahoxw3q2bZ 26-4-25]$ cat Makefile proc:proc.c gcc -o $@ $^ .PHONY:clean clean: rm -f proc [user1@iZ5waahoxw3q2bZ 26-4-25]$ cat proc.c #include<stdio.h> int main() { //printf("hello world\n"); FILE *fp =fopen("log.txt","r"); if(fp==NULL) return 1; //读取 fclose(fp); return 0; }
[user1@iZ5waahoxw3q2bZ 26-4-25]$ make gcc -o proc proc.c [user1@iZ5waahoxw3q2bZ 26-4-25]$ ./proc ----进程->父:bash

一般返回给父进程

程序没结果,怎么知道程序是对是错?

[user1@iZ5waahoxw3q2bZ 26-4-25]$ ./proc [user1@iZ5waahoxw3q2bZ 26-4-25]$ echo $? 1

echo $? 的含义是:打印上一条命令的退出状态码。

proc进程结果由它的父进程bash关心
父进程要获取对应子进程的退出码
而$?表示打印最近一个程序(进程)退出时的退出码---进程退出码!--写到你的进程的task_struct内部的!

[user1@iZ5waahoxw3q2bZ 26-4-25]$ echo $? 1 [user1@iZ5waahoxw3q2bZ 26-4-25]$ echo $? 0

为什么第二次输入就变成0了

---拿到上一个echo命令的退出码,上一个echo命令执行成功,返回退出码0

退出码

退出码(退出状态)可以告诉我们最后一次执行的命令的状态。在命令结束以后,我们可以知道命令 是成功完成的还是以错误结束的。其基本思想是,程序返回退出代码 0 时表示执行成功,没有问题。 代码 1 或 0 以外的任何代码都被视为不成功。

错误和错误码对应的描述strerror

NAME strerror, strerror_r - return string describing error number SYNOPSIS #include <string.h> char *strerror(int errnum); int strerror_r(int errnum, char *buf, size_t buflen); /* XSI-compliant */ char *strerror_r(int errnum, char *buf, size_t buflen); /* GNU-specific */ Feature Test Macro Requirements for glibc (see feature_test_macros(7)): The XSI-compliant version of strerror_r() is provided if: (_POSIX_C_SOURCE >= 200112L || _XOPEN_SOURCE >= 600) && ! _GNU_SOURCE Otherwise, the GNU-specific version is provided.
[user1@iZ5waahoxw3q2bZ 26-4-25]$ cat proc.c #include<stdio.h> #include<string.h> int main() { int i =0; for(;i<200;i++) { printf("%d->%s\n",i,strerror(i)); } return 0; }
[user1@iZ5waahoxw3q2bZ 26-4-25]$ ./proc 0->Success 1->Operation not permitted 2->No such file or directory 3->No such process 4->Interrupted system call 5->Input/output error 6->No such device or address 7->Argument list too long 8->Exec format error 9->Bad file descriptor 10->No child processes 11->Resource temporarily unavailable 12->Cannot allocate memory 13->Permission denied 14->Bad address 15->Block device required 16->Device or resource busy 17->File exists 18->Invalid cross-device link 19->No such device 20->Not a directory 21->Is a directory 22->Invalid argument 23->Too many open files in system 24->Too many open files 25->Inappropriate ioctl for device 26->Text file busy 27->File too large 28->No space left on device 29->Illegal seek 30->Read-only file system 31->Too many links 32->Broken pipe 33->Numerical argument out of domain 34->Numerical result out of range 35->Resource deadlock avoided 36->File name too long 37->No locks available 38->Function not implemented 39->Directory not empty 40->Too many levels of symbolic links 41->Unknown error 41 42->No message of desired type 43->Identifier removed 44->Channel number out of range 45->Level 2 not synchronized 46->Level 3 halted 47->Level 3 reset 48->Link number out of range 49->Protocol driver not attached 50->No CSI structure available 51->Level 2 halted 52->Invalid exchange 53->Invalid request descriptor 54->Exchange full 55->No anode 56->Invalid request code 57->Invalid slot 58->Unknown error 58 59->Bad font file format 60->Device not a stream 61->No data available 62->Timer expired 63->Out of streams resources 64->Machine is not on the network 65->Package not installed 66->Object is remote 67->Link has been severed 68->Advertise error 69->Srmount error 70->Communication error on send 71->Protocol error 72->Multihop attempted 73->RFS specific error 74->Bad message 75->Value too large for defined data type 76->Name not unique on network 77->File descriptor in bad state 78->Remote address changed 79->Can not access a needed shared library 80->Accessing a corrupted shared library 81->.lib section in a.out corrupted 82->Attempting to link in too many shared libraries 83->Cannot exec a shared library directly 84->Invalid or incomplete multibyte or wide character 85->Interrupted system call should be restarted 86->Streams pipe error 87->Too many users 88->Socket operation on non-socket 89->Destination address required 90->Message too long 91->Protocol wrong type for socket 92->Protocol not available 93->Protocol not supported 94->Socket type not supported 95->Operation not supported 96->Protocol family not supported 97->Address family not supported by protocol 98->Address already in use 99->Cannot assign requested address 100->Network is down 101->Network is unreachable 102->Network dropped connection on reset 103->Software caused connection abort 104->Connection reset by peer 105->No buffer space available 106->Transport endpoint is already connected 107->Transport endpoint is not connected 108->Cannot send after transport endpoint shutdown 109->Too many references: cannot splice 110->Connection timed out 111->Connection refused 112->Host is down 113->No route to host 114->Operation already in progress 115->Operation now in progress 116->Stale file handle 117->Structure needs cleaning 118->Not a XENIX named type file 119->No XENIX semaphores available 120->Is a named type file 121->Remote I/O error 122->Disk quota exceeded 123->No medium found 124->Wrong medium type 125->Operation canceled 126->Required key not available 127->Key has expired 128->Key has been revoked 129->Key was rejected by service 130->Owner died 131->State not recoverable 132->Operation not possible due to RF-kill 133->Memory page has hardware error 134->Unknown error 134 135->Unknown error 135 136->Unknown error 136 137->Unknown error 137 138->Unknown error 138 139->Unknown error 139 140->Unknown error 140 141->Unknown error 141 142->Unknown error 142 143->Unknown error 143 144->Unknown error 144 145->Unknown error 145 146->Unknown error 146 147->Unknown error 147 148->Unknown error 148 149->Unknown error 149 150->Unknown error 150 151->Unknown error 151 152->Unknown error 152 153->Unknown error 153 154->Unknown error 154 155->Unknown error 155 156->Unknown error 156 157->Unknown error 157 158->Unknown error 158 159->Unknown error 159 160->Unknown error 160 161->Unknown error 161 162->Unknown error 162 163->Unknown error 163 164->Unknown error 164 165->Unknown error 165 166->Unknown error 166 167->Unknown error 167 168->Unknown error 168 169->Unknown error 169 170->Unknown error 170 171->Unknown error 171 172->Unknown error 172 173->Unknown error 173 174->Unknown error 174 175->Unknown error 175 176->Unknown error 176 177->Unknown error 177 178->Unknown error 178 179->Unknown error 179 180->Unknown error 180 181->Unknown error 181 182->Unknown error 182 183->Unknown error 183 184->Unknown error 184 185->Unknown error 185 186->Unknown error 186 187->Unknown error 187 188->Unknown error 188 189->Unknown error 189 190->Unknown error 190 191->Unknown error 191 192->Unknown error 192 193->Unknown error 193 194->Unknown error 194 195->Unknown error 195 196->Unknown error 196 197->Unknown error 197 198->Unknown error 198 199->Unknown error 199
[user1@iZ5waahoxw3q2bZ 26-4-25]$ cat proc.c #include<stdio.h> #include<string.h> #include<errno.h> int main() { int i =0; for(;i<200;i++) { printf("%d->%s\n",i,strerror(i)); } FILE *fp =fopen("log.txt","r"); if(fp==NULL) return errno; return 0; }
[user1@iZ5waahoxw3q2bZ 26-4-25]$ ./proc 。。。。。。。。。 [user1@iZ5waahoxw3q2bZ 26-4-25]$ echo $? 2

2->No such file or directory
2代表打开文件没有目录。

[user1@iZ5waahoxw3q2bZ 26-4-25]$ ls -l hello.txt ls: cannot access hello.txt: No such file or directory [user1@iZ5waahoxw3q2bZ 26-4-25]$ echo $? 2

当然了也可以直接定制,比如想返回7那就是7

[user1@iZ5waahoxw3q2bZ 26-4-25]$ cat proc.c #include<stdio.h> #include<string.h> #include<errno.h> int main() { int i =0; for(;i<200;i++) { printf("%d->%s\n",i,strerror(i)); } FILE *fp =fopen("log.txt","r"); if(fp==NULL) return 7; return 0; } [user1@iZ5waahoxw3q2bZ 26-4-25]$ make gcc -o proc proc.c [user1@iZ5waahoxw3q2bZ 26-4-25]$ ./proc 。。。。。。。。。。。 [user1@iZ5waahoxw3q2bZ 26-4-25]$ echo $? 7

但一旦出现代码异常,退出码无意义!

TODO

进程一旦出现异常,一般就是进程收到了信号

Linux Shell 中的主要退出码:

退出码解释
0命令成功执行
1通用错误代码
2命令(或参数)使用不当
126权限被拒绝(或)无法执行
127未找到命令,或 PATH 错误
128+n命令被信号从外部终止,或遇到致命错误
130通过 Ctrl+C 或 SIGINT 终止(终止代码 2 或键盘中断)
143通过 SIGTERM 终止(默认终止)
255/(*)退出码超过了 0-255 的范围,因此重新计算(LCTT 译注:超过 255 后,用退出码取模)

• 退出码 0 表示命令执行无误,这是完成命令的理想状态。

• 退出码 1 我们也可以将其解释为 “不被允许的操作”。例如在没有 sudo 权限的情况下使用 yum;再例如除以 0 等操作也会返回错误码 1 ,对应的命令为 let a=1/0

• 130 ( SIGINT 或 ^C )和 143 ( SIGTERM )等终止信号是非常典型的,它们属于 128+n 信号,其中 n 代表终止码。

• 可以使用 strerror 函数来获取退出码对应的描述。

main函数结束,表示进程结束

其他函数return,只表示自己函数调用完成,返回

_exit函数

#include<unsitd.h>

void _exit(int status);

参数:status 定义了进程的终止状态,父进程通过wait来获取该值

• 说明:虽然status是int,但是仅有低8位可以被父进程所用。所以_exit(-1)时,在终端执行$?发现 返回值是255。

exit

表示进程终止

NAME exit - cause normal process termination SYNOPSIS #include <stdlib.h> void exit(int status);

1.

[user1@iZ5waahoxw3q2bZ 26-4-25]$ cat proc.c #include<stdio.h> #include<string.h> #include<errno.h> #include<stdlib.h> int main() { exit(23); return 0; } [user1@iZ5waahoxw3q2bZ 26-4-25]$ make gcc -o proc proc.c [user1@iZ5waahoxw3q2bZ 26-4-25]$ ./proc [user1@iZ5waahoxw3q2bZ 26-4-25]$ echo $? 23

2.

[user1@iZ5waahoxw3q2bZ 26-4-25]$ cat proc.c #include<stdio.h> #include<string.h> #include<errno.h> #include<stdlib.h> void fun() { printf("fun begin!\n"); exit(40); printf("fun end!\n"); } int main() { fun(); printf("main!\n"); return 0; }
[user1@iZ5waahoxw3q2bZ 26-4-25]$ make gcc -o proc proc.c [user1@iZ5waahoxw3q2bZ 26-4-25]$ ./proc fun begin! [user1@iZ5waahoxw3q2bZ 26-4-25]$ echo $? 40

任何地方调用exit,都表示进程结束!并返回给父进程bash,子进程的退出码!

exit vs _exit

exit是c语言提供

_exit是系统提供

进程如果是exit退出的时候,exit(),进程退出的时候,会进行缓冲区的刷新

进程如果是_exit退出的时候,_exit(),进程退出的时候,不会进行缓冲区的刷新

我们之前谈论的缓冲区,应该在哪里?

或者一定不在哪里?

---一定不是在操作系统内部的缓冲区!

库缓冲区,C语言提供的缓冲区

库函数和系统调用是上下层的关系

库函数会调用相关的系统调用来完成任务

exit底层封装了_exit

exit最后也会调用_exit, 但在调用_exit之前,还做了其他工作:

1. 执行用户通过 atexit或on_exit定义的清理函数。

2. 关闭所有打开的流,所有的缓存数据均被写入

3. 调用_exit

return退出

return是一种更常见的退出进程方法。执行return n等同于执行exit(n),因为调用main的运行时函数会 将main的返回值当做 exit的参数。

感谢你的观看,欢迎我们下篇再见!

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/25 10:31:56

3个步骤让你的旧iPhone重获新生:Legacy-iOS-Kit终极降级指南

3个步骤让你的旧iPhone重获新生&#xff1a;Legacy-iOS-Kit终极降级指南 【免费下载链接】Legacy-iOS-Kit An all-in-one tool to restore/downgrade, save SHSH blobs, jailbreak legacy iOS devices, and more 项目地址: https://gitcode.com/gh_mirrors/le/Legacy-iOS-Kit…

作者头像 李华
网站建设 2026/4/25 10:31:17

别再傻傻分不清了!嵌入式开发中GPIO配置和PINCTRL到底怎么用?一个设备树例子讲透

嵌入式开发中的GPIO与PINCTRL&#xff1a;从概念到实战的设备树配置指南 在嵌入式系统开发中&#xff0c;硬件引脚的配置往往是让开发者既爱又恨的部分。爱的是它直接与硬件交互的能力&#xff0c;恨的是不同芯片厂商、不同平台之间的配置方式千差万别。特别是当你在设备树中看…

作者头像 李华
网站建设 2026/4/25 10:28:49

避坑指南:SAP采购申请批量审批/反审批,这些BAPI调用细节千万别忽略

SAP采购申请批量审批实战避坑指南&#xff1a;BAPI调用中的高阶技巧 当你面对数百条采购申请需要批量审批时&#xff0c;一个看似简单的BAPI调用可能变成一场噩梦。权限报错、锁定冲突、状态不一致——这些问题往往在深夜支持电话中突然出现。本文将分享我在多个SAP实施项目中积…

作者头像 李华
网站建设 2026/4/25 10:27:39

从一道省赛题到实战:用二分查找解决‘买木头’问题(附C++代码详解)

从算法竞赛到工程实践&#xff1a;二分查找在资源分配问题中的高阶应用 最近在辅导学生准备编程竞赛时&#xff0c;我发现很多选手对二分查找的理解停留在表面——他们知道如何在有序数组中查找元素&#xff0c;却无法将这个看似简单的算法应用到更复杂的实际问题中。这让我想起…

作者头像 李华