个人做网站还是公众号赚钱好,海南短视频搜索seo哪家实惠,设计类网站建设规划书,网站开发前端和后端的区别目录 前言#xff1a;
一、进程创建
1.1 fork函数
1.2 fork函数返回值
1.3 写时拷贝
1.4 fork的常规用法
二、进程终止
2.1、退出码
2.2、进程终止的方式 三、进程等待
3.1、等待的必要性
3.2 wait与waitpid
wait
waitpid
status的提取问题 四、重谈进程退出
…目录 前言
一、进程创建
1.1 fork函数
1.2 fork函数返回值
1.3 写时拷贝
1.4 fork的常规用法
二、进程终止
2.1、退出码
2.2、进程终止的方式 三、进程等待
3.1、等待的必要性
3.2 wait与waitpid
wait
waitpid
status的提取问题 四、重谈进程退出
总结 前言
前面关于进程的五篇博客主要给大家讲解的是一些进程的相关概念从本篇开始将为大家带来有关我们进程控制的内容。
我们怎么在我们的代码中控制、管理我们的进程呢进程有哪些基本的用图呢
希望通过本篇博客能够给大家解决这些疑惑。 一、进程创建
1.1 fork函数
在linux系统中fork是一个十分重要的函数它从一个已经存在的进程中创建一个新的进程。这个进度进程为原进程的子进程原进程为父进程。 进程调用fork当控制转移到内核中的fork代码后内核会做四件事情 1、分配新的内存块与内核数据结构PCB给子进程 2、将父进程部分数据结构内容拷贝到子进程 3、添加子进程到系统进程列表中 4、fork返回开始调度器调度 当一个进程调用fork之后就有两个二进制代码相同的进程。而且他们都运行到了相同的地方复制了指令指针寄存器EIP/RIP相当于程序执行的书签。但每个进程都将可以开始他们自己的旅程。 譬如我们有以下代码 #includestdio.h
#includeunistd.h
#includesys/types.hint main()
{printf(我是进程%d\n,getpid());pid_t id fork();if(id 0){printf(我是子进程%d\n,getpid());}else{printf(我是父进程%d\n,getpid());}
} 运行的结果就是 可以看见子进程从fork返回后开始执行不会重复fork之前的代码。这是因为内核复制了父进程的执行上下文包括EIP/RIP寄存器让子进程直接从fork的下一条指令继续运行。父子进程的执行顺序由内核调度器决定不同系统可能有不同表现。 1.2 fork函数返回值 fork函数的返回值主要有三种当fork运行错误时会返回-1。当运行成功时对于子进程会返回0对于父进程会返回子进程的pid 1.3 写时拷贝 通常父子进程的代码是共享的。当父子进程都不再写入数据时他们的数据也是共享的。当任意一方试图写入数据就会触发写时拷贝使其各自的数据分开来。 我在上文讲到过isexist借此也给大家介绍了写时拷贝的机制它本质上是依靠页表来实现的。 在fork之后父进程首先要做的事将代码与数据的权限全部改为只读所以子进程的权限也是只读。子进程尝试修改页表会识别对只读区域进行写入触发系统错误导致 缺页中断让系统去做检测如果修改的是代码区那就直接杀进程如果是数据区因为数据区本身就是读写权限可现在是只读那就判定为发生写时拷贝。 为什么要在申请新的内存空间的基础上将原本的数据拷贝一份呢因为新的数据的修改可能是在原本数据上的覆盖比如count。 写时拷贝,是⼀种延时申请技术,可以提高整机内存的使用率。因为有写时拷贝技术的存在,所以父子进程得以彻底分离完成了进程独立性的技术保证! 1.4 fork的常规用法 说到底fiork有两种常规的用法 1、一个父进程希望创建子进程复制自己随后使得父子进程执行不同的代码段。例如父进程等待客户端请求生成子进程来处理请求。 #includestdio.h
#includeunistd.h
#includesys/types.hint main()
{pid_t id fork();if(id 0){printf(我是子进程%d,我的父进程是%d\n,getpid(),getppid());printf(执行子进程专属代码);sleep(3);}else if(id 0){printf(我是父进程%d,我的子进程是%d\n,getpid(),id);printf(执行父进程专属代码);sleep(1);}else{printf(fork失败\n);}return 0;
} 2、一个进程要执行一个不同的程序。例如子进程从fork返回后调用exec等一系列的函数。 #includestdio.h
#includeunistd.h
#includesys/types.h
#include sys/wait.h
#includecstdlibint main()
{pid_t pid fork();if (pid 0) {// 子进程加载新程序execl(/bin/ls, ls, -l, NULL);// 若exec失败才会执行以下代码perror(exec failed);_exit(EXIT_FAILURE); // 必须用_exit避免刷新父进程缓冲区} else if (pid 0) {// 父进程继续执行原程序waitpid(pid, NULL, 0); printf(子进程已退出\n);}return 0;
} 大家可能对exec系列的函数不太熟悉包括我们所使用的配套的waitpid没关系我们会在后面的内容进行讲解这里只是给大家示范一下第二种用法。 二、进程终止
进程终止是操作系统管理进程生命周期的关键环节其本质是释放系统资源就是释放进程申请的相关内核数据结构和对应的数据与代码。
2.1、退出码
在C标准库中存在着errno与strerror这对组合。error是一个定义在errno.h的全局变量strerror通过传进来的出错误码errno可以给出字符串。通过打印这个字符串就能够让我们知道错误的原因。
errno 负责捕获错误本质strerror 负责翻译错误真相二者协作构成了 Linux 系统编程中高效、精准、标准化的错误处理基石。
例如在文件操作中我们有时经常出现错误
int main()
{//打开文件前的错误信息printf(errno: %d ,errstring: %s\n,errno,strerror(errno));FILE*fpfopen(./log.txt,r);if(fpnullptr){//打开文件失败后的错误信息printf(errno: %d ,errstring: %s\n,errno,strerror(errno));return errno;}return 0;
}
在我系统下没有./log.txt时就会出错此时我们就能获取原因No such file or directory不存在这样一个文件或者目录
那么系统中给我们提供了多少个错误码呢
根据操作系统的不同有着不同的错误码我们看一眼试着打印一下linux系统的
int main()
{for(int i1;i200;i){std::couti: strerror(i)std::endl;}return 0;
} 结果如下
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
...省略
195: Unknown error 195
196: Unknown error 196
197: Unknown error 197
198: Unknown error 198
199: Unknown error 199
可以看得出来有133个是系统规定了的错误类型。
同学们那我们就必须要使用系统给的错误码吗
不是的我们写的代码跟系统强相关自然可以使用系统给的错误码。写的毫无关系自然可以自己规定一套错误码。
2.2、进程终止的方式
一般来说程序正常终止有三种情况我们在linux系统下可以通过echo $?指令来查看进程的退出码 1、从main函数返回 2、任意地方调用exit 3、_exit终止 异常退出 ctrl c退出信号终止 在退出的时候程序可以给我们返回退出码随后让我们的值他是否完成了预期的任务。其基本思想就是我们规定程序返回代码0时才表示执行成功返回其他任意代码都被视为不成功。 大家在学习C语言或者C语言时曾经天天都在写main函数。main函数也是一个函数自然有自己的返回值。main函数的返回值类型主要有两种类型最主要的是int main另外一个是void mainvoid就是没有返回值。那么大家有没有想过为什么main函数会有返回值呢这个返回值又能给谁呢
答案是返回给父进程或者系统。
我们通过子进程的main函数的返回值就能确定子进程的执行的结果。这其实也是我们前面提到过一点的。return n等同于执⾏exit(n),因为调⽤main的运⾏时函数会将main的返回值当做 exit的参数。
main函数中的exit0可以平替return 0但是在其他地方不能平替。
比如非main函数的return其实只代表这个函数的结束但是非main函数调用exit代表进程的结束。
而exit与_exit呢他们的主要区别还是在于exit最后也会调用_exit, 但在调用_exit之前还做了其他⼯作
1、执⾏⽤⼾通过 atexit或on_exit定义的清理函数。
2、关闭所有打开的流所有的缓存数据均被写入 3、 调⽤_exit 我们给大家做个演示
int main()
{printf(你好这是一个打印);exit(0);
}
这个程序的运行结果是
而我们使用_exit()
int main()
{printf(你好这是一个打印);_exit(0);
} 什么也都不会打印,这是因为exit() 属于标准 C 库函数它会 1、刷新所有 stdio 缓冲区包括 printf 未刷新的输出 2、 调用通过 atexit() 注册的清理函数 3、 最终调用 _exit() 系统调用终止进程 而_exit() 是直接的系统调用定义在 unistd.h它会 1、立即终止进程不刷新任何缓冲区 2、跳过所有标准库的清理流程 3、由于 printf 的输出在行缓冲模式下无换行符 \n未被刷新内容丢失 三、进程等待
一般而言父进程创建了子进程父进程就要等待子进程直到子进程结束。
3.1、等待的必要性
为什么需要父进程来等待呢
之前在进程状态时讲过子进程退出如果父进程不管不顾就可能造成僵尸进程的问题进而造成内存泄漏。
另外进程一旦变成僵尸状态那就刀枪不入哪怕使用kill函数调用也无能为力因为谁也没有办法杀死一个已经死去的进程。
最后父进程派给子进程的任务完成的怎么样了我们需要知道如子进程运行完成结果是对是错或者是否正常退出。
总的来说父进程通过进程等待的方式回收子进程资源获取子进程退出的相关信息。
3.2 wait与waitpid
我们主要有两个函数来进行父进程的等待wait与waitpid pid_t wait(int *status); pid_t waitpid(pid_t pid, int *status, int options); status是一个指向整数类型的指针用于存储子进程的退出信息如果为空指针就表示不关心子进程的退出状态他是一个输出型的参数我们可以用这个参数来检查子进程的退出状态。
函数运行成功后会返回终止的子进程的PID如果失败了就返回-1并且设置errno。 使用wait函数时当我们的子进程一直不退出父进程就会阻塞到wait函数内部。
对于waitpid的参数pid指定要等待的子进程 pid大于0时等待进程ID等于pid的子进程pid为-1时代表任意一个子进程类似于wait等于0时等待与调用进程同进程组的任意子进程小于0时等待进程组ID等于pid绝对值的任意子进程。
status参数同上是一个输出型参数。options参数用于控制函数行为可组合使用 WNOHANG非阻塞模式如果没有子进程退出立即返回0 WUNTRACED也返回停止的子进程状态 WCONTINUED也返回已继续的子进程状态 我们写点代码来认识一下这几个函数
wait int main()
{pid_t id fork();if(id 0){int cnt10;while(cnt--){printf(子进程运行中%d\n,getpid());sleep(1);} }else if(id 0){pid_t ridwait(nullptr);if(rid0){printf(等待成功rid%d\n,rid);}while(1){printf(我是父进程%d\n,getpid());sleep(1);}}else{printf(errno: %d ,errstring: %s\n,errno,strerror(errno));return errno;}return 0;
}
这个代码创建了一个子进程运行十秒钟之后让父进程用wait来回收子进程
运行以上代码在另外一个bash中输入以下指令进行循环的进程状态查看 while :; do ps ajx | head -1 ps ajx | grep test; sleep 1; done 我们可以看到这个结果 可以看见过程中并未出现僵尸状态因为僵尸已经被我们父进程回收了。
那我们在使用一下waitpid吧我先将以上的代码简单的更改一下
waitpid
int main()
{pid_t id fork();if(id 0){int cnt3;while(cnt--){printf(子进程运行中%d\n,getpid());sleep(1);} exit(123);//我们设定一下子进程的退出码为123}else if(id 0){//pid_t ridwait(nullptr);int status;pid_t ridwaitpid(id,status,0);if(rid0){printf(Child %d exited with status %d\n,rid, status);//试着让父进程打印一下status}while(1){printf(我是父进程%d\n,getpid());sleep(1);}}else{printf(errno: %d ,errstring: %s\n,errno,strerror(errno));return errno;}return 0;
}
我们故意让子进程退出码信息为123然后再父进程回收时打印status却发现打印的status的结果不是123 这是怎么回事呢
status的提取问题 status不能简单的当作整形来看待可以当作位图来看待具体细节如下图只研究status低16 比特位 也就是说我们想要查看进程的退出码我们需要提取出低16位的高八位status 8 0xFF,我们也有一个标准宏来实现这个过程WEXITSTATUSstatus。 阻塞options的问题我们之后在谈 四、重谈进程退出
了解了以上的知识我们就知道了一般来说进程退出有三种情况 1、代码跑完了结果是对的返回0。
2、代码跑完了结果不对返回非0。
3、代码没跑完异常了。 前两种是我们一直在说的通过退出码判定最后一种这是最复杂的情况通常是被信号(Signal)终止的 进程退出信息会记录退出的退出信号低7位。也就是说status会同时记录退出码与退出信号同样可以通过标准宏的手段让我们知道退出信号是什么。 常见的退出信号原因包括但不限于 除零错误 (SIGFPE) 段错误 (SIGSEGV) 用户中断 (SIGINT, 如CtrlC) 强制终止 (SIGKILL) 总线错误 (SIGBUS) 以野指针来说我们之前提到过 野指针实际上指向的是虚拟地址在使用的时候可能会出现指向的那个地址权限不对或者根本不存在对应映射所以可能会杀掉进程 导致程序崩溃。
杀掉进程的手段就会触发段错误(Segmentation Fault)系统发送SIGSEGV信号信号编号11给进程。
总结
我们本篇文章主要是讲了进程创建进程的终止以及进程等待的部分内容了解了写时拷贝以及退出码等信息。关于进程的等待还有一点内容options参数并未向大家详细介绍这个我会放在下一篇文章讲解。
关于进程的控制这一章节我打算划分两篇给大家讲解下一篇的内容应该包含进程等待的阻塞选项问题然后会为大家讲一下进程的替换的内容这一部分非常重要并借此给大家简单展示一下每隔xx秒就自动存档记录的原理。讲完进程替换就会为大家基于我们之前所学的所有内容做一个进程的总结并以此写一个我们shell的模拟实现就是我们平时使用的SHell命令行解释器的模拟实现。
鼠鼠我最近要备战期末周所以更新肯定会延迟到月底去了希望大家多多包含祝福我高数下别挂科我高数上都没学过就直接让我学高数下吗有意思。
月底再跟大家见面啦希望本篇文章的内容能够对您有所帮助谢谢