一、进程创建
1.1 fork函数的认识
#include<unistd.h>
pid_t fork(void);
返回值:自进程返回0,父进程返回子进程PID,出错返回-1
- 进程调用fork,当控制转移到内核中的fork代码后,内核做:
- 分配新的内存块和内核数据结构给子进程;
- 将父进程部分数据结构内容拷贝至子进程;
- 添加子进程到系统进程列表中;
- fork返回,开始调度器调度。
当一个进程调用fork后,就有两个二进制代码相同的进程;而且它们都运行到相同的地方;但每个进程都将可以开始它们自己的旅程。
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<errno.h>
#include<stdlib.h>
int main(){
pid_t pid;
printf("before: pid is %d\n", getpid());
if((pid = fork()) == -1){
perror("fork()");
exit(1);
}
printf("after: pid is %d, fork return %d\n", getpid(), pid);
sleep(1);
return 0;
}
调试结果:
从图2中可以看到:进程2981打印before消息,然后它有打印after。另一个after消息由2982打印的。注意到进程2982没有打印before,见图3:
所以,fork之前父进程独立执行,fork之后,父进程、子进程两个执行流分别执行。注意,fork之后,谁先执行完全由调度器决定。
1.2 写时拷贝
通常,父子代码共享,父子在不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自一份副本,具体详见下图:
1.3 fork常规用法
-
一个父进程希望复制自己,使父子进程同时执行不同的段码段。例如,父进程等待客户端请求,生成子进程来处理请求;
-
一个进程要执行一个不同的程序。例如子进程从fork返回后,调用exec函数。
1.4 fork调用失败原因
-
系统中有太多的进程;
-
实际用户的进程数超出了限制。
二、进程终止
2.1 进程退出场景
a.正常执行完(1.结果正确;2.结果不正确);
b.崩溃了(进程异常)【具体在进程信号处详细说明】 – 崩溃的本质:进程因为某些原因,导致进程收到来自操作系统的信号(kill -9)。
在main()函数中,return 0;
中0是进程的退出码,表示正常执行完了(1.结果正确(0);2.结果不正确(!0非零;1,2,3,4 --> 表示不同原因))-> 供用户进行进程退出健康状态的判定。
#include<stdio.h>
#include<string.h>
int main(){
for(int i = 0; i <= 200; i++){
printf("%d : %s\n", i, strerror(i));
//char* strerror(int errnum); --> #include<string.h>
}
return 0;
}
上述代码执行的是遍历Linux系统(主要指CentOS7)中进程退出码,具体如图6所示,有(0~133)即134个。
进程退出:就是操作系统内少了一个进程,操作系统就要释放进程对应的内核数据结构+代码和数据(如果有独立的)。
2.2 进程常见退出方法
2.2.1 常见的三种方法
- main( )函数return,其他函数return 仅仅代表该函数返回 -> 进程执行,本质是main执行流执行!
- exit()函数退出 – C标准库函数调用
- _exit() – 系统函数调用
2.2.2 方法的刨析
exit(int code){
//code代表就是进程的退出码,等价于main(){return xxx};
//冲刷缓冲区
_exit(code);
}
在代码的任意地方调用该函数都表示进程退出!,具体下列代码所示:
#include<stdlib.h>
int main(){
for(int i = 0; i <= 140; i++){
printf("%d : %s\n", i, strerror(i));
exit(123);
}
return 0;
}
int add_to_top(int top){
printf("enter add_to_top\n");
int sum = 0;
for(int i = 0; i <= top; i++){
sum += i;
}
exit(213);
printf("out add_to_top\n");
return sum;
}
int main(){
int result = add_to_top(100);
if(result == 5050){
return 0;
}else{
return 11;//计算结果不正确
}
}
注意:exit()
函数退出,在代码的任意地方调用该函数都表示进程退出。
_exit()
函数,貌似等价于exit(),但不会清理缓冲区
_exit(int status)
exit()函数的调用举例:
int main(){
printf("welcome to TU Berlin\n");//输出缓冲区
sleep(2);
exit(107);//关闭文件+冲刷缓冲区
return 0;
}
_exit()函数的调用举例:
int main(){
printf("welcome to TU Berlin");//输出缓冲区
sleep(2);
_exit(108);//干掉进程,不会对缓冲区数据做任何刷新
return 0;
}
exit(int code){
//冲刷缓冲区
_exit(code);
}
这里我推荐使用exit()函数,上述代码可知exit()函数是_exit()
的封装,但_exit()
函数退出时没有刷新缓冲区。那么缓冲区在哪里?这个缓冲区不在操作系统内部,而是在C库。
异常退出
- ctrl + c,信号终止
./mytest //mytest变成一个子进程,父进程是bash
三、进程等待
3.1 进程等待必要性
- 子进程退出,若父进程不管不顾就会造成“僵尸进程”,从而引起内存泄漏;
- 另外,进程一旦变成僵尸进程,那就刀枪不入,
kill -9
也无能为力,因为谁也无法杀死一个已死的进程;- 最后,父进程派给子进程的任务完成的如何,我们需要知道。如子进程运行完成,结果正确还是错误,亦或是否正常退出;
- 父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息。
等待本质就是通过系统调用,获取子进程退出码或退出信号的方式。
3.2 进程等待的方法
3.2.1 wait方法
#include<sys/types.h>
#include<sys/wait.h>
pid_t wait(int *status);
返回值:成功返回被等待进程pid,失败则返回-1.
参数:输出型参数,获取子进程退出状态,不关心则可设置成NULL.
3.2.2 waitpid方法
返回值:
- 当正常返回的时候,waitpid返回收集到的子进程的进程ID;
- 如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子集可收集,则返回0;
- 如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在。
注意:status是输出型参数。
参数:
pid:
pid=-1,等待任一个子进程,与wait等效;
pid>0,等待其进程ID与pid相等的子进程。
status:
WIFEXITED(status):若为正常终止子进程返回的状态,则为真
WEXITSTATUS(status):若WIFEXITED非零,提取子进程退出码
options:
- WNOHANG:若pid指定的子进程没有结束,则waitpid()返回0,不予以等待;若正常结束,则返回该子进程的ID;
若子进程已经退出,调用wait/waitpid时,wait/waitpid会立即返回,并释放资源,获得子进程退出信息;
若在任意时刻调用wait/waitpid,子进程存在且正常运行,则进程可能阻塞;
若不存在该子进程,则立即出错返回。
3.2.3 获取子进程status
- wait和waitpid,都是一个status参数,该参数是一个输出型参数,由操作系统填充;
- 若传递NULL,则表示不关心子进程的退出状态信息;
- 否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程;
- status不能简单的当作整型来看待,可以当作位图来看待,具体细节如下图(只研究status低16比特位)
图15中,0表示没有收到信号,正常退出 -> 退出码(0 1 2…)。下列的实例1为wait方法的使用:
int main(){
pid_t id = fork();
if(id == 0){
//子进程
int cnt = 5;
while(cnt){
printf("我是子进程, 我还活着呢, 我还有%ds, PID:%d, PPID: %d\n",cnt--, getpid(), getppid());
sleep(1);
}
exit(0);
}
sleep(10);
//父进程
pid_t ret_id = wait(NULL);
printf("我是父进程, 等待子进程成功, PID: %d, PPID: %d, ret_id: %d\n",getpid(), getppid(), ret_id);
sleep(5);
}
实例2为退出码与退出信号的simulate:
int main(){
pid_t id = fork();
if(id == 0){
//子进程
int cnt = 5;
while(cnt){
printf("我是子进程, 我还活着呢, 我还有%ds, PID:%d, PPID: %d\n",cnt--, getpid(), getppid());
sleep(1);
}
exit(0);
}
sleep(10);
//父进程
int status = 0;
pid_t ret_id = waitpid(id, &status, 0);
printf("我是父进程, 等待子进程成功, PID: %d, PPID: %d, ret_id: %d, child exit status: %d, child exit signal: %d\n",\
getpid(), getppid(), ret_id, (status>>8)&0xFF, status & 0x7F);
sleep(5);
// return 0;
}
父进程在wait的时候,若子进程没有退出,父进程只能一直在调用waitpid进行等待(阻塞等待)。
四、进程程序替换
4.1 替换原理
用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行,调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。
4.2 替换函数
将以exec开头的函数,统称为exec函数:
#include<unistd.h>
int execl(const char* path, const char* arg, …);
int execv(const char* path, char* const argv[]);int execlp(const char* file, const char* arg, …);
int execvp(const char* file, char* const argv[]);int execle(const char* path, const char* arg, …, char *const envp[]);
int execve(const char* path, char* const argv[], char* const envp[]);
4.2.1 函数解释
- 这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回;
- 若果调用出错,则返回-1;
- 所以exec函数只有出错的返回值,而没有成功的返回值。
4.2.2 命名理解
这些函数原型看起来很容易混,若掌握规律则很好记忆:
- l(list): 表示参数采用列表
- v(vector): 参数用数组
- p(path): 有p自动搜索环境变量PATH
- e(env): 表示自己维护环境变量
函数名 | 参数格式 | 是否带路径 | 是否使用当前环境变量 |
---|---|---|---|
execl | 列表 | NO | 是 |
execlp | 列表 | YES | 是 |
execle | 列表 | NO | 不是,须自己组装env |
execv | 数组 | NO | 是 |
execvp | 数组 | YES | 是 |
execve | 数组 | NO | 不是,须自己组装env |
4.3 替换函数案例
4.3.1 execl调用举例
int main(){
pid_t id = fork();
assert(id >= 0);
(void)id;
if(id ==0){
printf("我是子程序, pid: %d, ppid: %d\n", getpid(), getppid());
execl("/bin/ls", "ls", "-a", "-l", NULL);//必须NULL结束
//执行程序替换,新的代码和数据就被加载了,后续的代码属于旧代码,直接被替换,没有机会执行了
printf("子程序还在呢!\n");
}
sleep(5);
while(1){
int status = 0;
pid_t ret_id = waitpid(id, &status, WNOHANG);
if(ret_id < 0){
printf("waitpid error!\n");
exit(0);
}else if(ret_id == 0){
printf("子程序还活着呢!\n");
continue;
}else{
printf("我是父进程, 我的pid: %d, child exit code: %d, child exit signal: %d\n",\
getpid(), (status>>8)&0xFF, status & 0x7F);
break;
}
}
}
注意:execl
,如果替换成功,不会有返回值;如果替换失败,一定返回值 。
4.3.2 execv调用举例
int main(){
pid_t id = fork();
assert(id >= 0);
(void)id;
if(id == 0){
printf("我是子程序, pid: %d, ppid: %d\n", getpid(), getppid());
char* const myargv[] = {
"ls",
"-a",
"-n",
NULL
};
execv("/bin/ls", myargv);//命令ls -an替换子进程
printf("you failed!\n");
}
sleep(5);
while(1){
int status = 0;
pid_t ret_id = waitpid(id, &status, WNOHANG);
if(ret_id < 0){
printf("waitpid error!\n");
exit(0);
}else if(ret_id == 0){
printf("子程序还活着呢!\n");
continue;
}else{
printf("我是父进程, 我的pid: %d, child exit code: %d, child exit signal: %d\n",\
getpid(), (status>>8)&0xFF, status & 0x7F);
break;
}
}
}
4.3.3 execlp调用举例
int execlp(const char* file, const char* arg,…);
p表示path(也是相对路径),当我们执行指定程序的时候,只需要指定程序名即可,系统会自动在环境变量PATH中进行查找。
int main(){
pid_t id = fork();
assert(id >= 0);
(void)id;
if(id == 0){
//child
printf("我是子进程, PID: %d\n", getpid());
execlp("ls", "ls", "-a", "-i", NULL);//命令ls -ai来替换子进程
printf("you fail\n");
}
//father
sleep(5);
int status = 0;
printf("我是父进程, PID: %d\n", getpid());
waitpid(id, &status, 0);
printf("child exit code: %d\n", WEXITSTATUS(status));
}
4.3.4 execvp调用举例
int execvp(const char* file, char* const argv[]);
execvp中p就不需要绝对路径
int main(){
pid_t id = fork();
if(id == 0){
//子进程
printf("我是子进程, PID: %d\n", getpid());
char* const myargv[] = {
"ls",
"-a",
"-l",
"-n",
NULL
};
execvp("ls",myargv);
printf("you failed\n");
}
//父进程
sleep(5);
int status = 0;
printf("我是父进程, PID: %d\n", getpid());
waitpid(id, &status, 0);
printf("child exit code: %d\n", WEXITSTATUS(status));
return 0;
}
4.3.5 execl替换的单个子进程
int main(){
pid_t id = fork();
if(id == 0){
//子进程
printf("我是子进程, PID: %d\n", getpid());
execl("./exec/otherproc","otherproc",NULL);
printf("you failed\n");
}
//父进程
sleep(5);
int status = 0;
printf("我是父进程, PID: %d\n", getpid());
waitpid(id, &status, 0);
printf("child exit code: %d\n", WEXITSTATUS(status));
return 0;
}
五、进程控制项目实战——模拟shell
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<assert.h>
#include<sys/types.h>
#include<sys/wait.h>
#define MAX 1024
#define ARGC 64
#define SEP " "
int split(char* commandstr, char* argv[]){
assert(commandstr);
assert(argv);
argv[0] = strtok(commandstr, SEP);
if(argv[0] == NULL){
return -1;
}
int i = 1;
while(1){
argv[i] = strtok(NULL,SEP);
if(argv[i] == NULL){
break;
}
i++;
}
return 0;
}
void debugPrint(char* argv[]){
for(int i = 0; argv[i]; i++){
printf("[%d]: %s\n", i, argv[i]);
}
}
void showEnv(){
extern char** environ;
for(int i = 0; environ[i]; i++){
printf("%d: %s\n", i, environ[i]);
}
}
int main(){
int last_exit = 0;
char myenv[32][256];
int env_index = 0;
while(1){
char commandstr[MAX] = {0};
char* argv[ARGC] = {NULL};
printf("[ChuHsiang@HuaWeiCloud]# ");
fflush(stdout);
// sleep(100);
char* s = fgets(commandstr, sizeof(commandstr), stdin);
assert(s);
(void)s;
commandstr[strlen(commandstr) - 1] = '\0';
int n = split(commandstr,argv);
if(n != 0){
continue;
}
// debugPrint(argv);
//version 2 : 说明几个细节
//cd .. /cd/ 等 bash自己执行的命令,称之为内建命令/内置命令 --> int chdir(const char *path);
if(strcmp(argv[0], "cd") == 0){
if(argv[1] != NULL){
//说到底,cd命令,重要的表现就如同bash自己调用了对应的函数
chdir(argv[1]);
}
continue;//不会往下继续执行,回到while(1)重新开始
}else if(strcmp(argv[0], "export") == 0){
if(argv[1] != NULL){
strcpy(myenv[env_index], argv[1]);
putenv(myenv[env_index++]);
}
continue;
}else if(strcmp(argv[0], "env") == 0){
showEnv();
continue;
}else if(strcmp(argv[0], "echo") == 0){
//echo $PATH
const char* target_env = NULL;
if(argv[1][0] == '$'){
if(argv[1][1] == '?'){
printf("%d\n", last_exit);
continue;
}else{
target_env = getenv(argv[1]+1);
}
}
if(target_env != NULL){
printf("%s=%s\n", argv[1]+1, target_env);
}
continue;
}
// else if(strcmp(argv[0], "export") == 0){
// if(argv[1] != NULL){
// putenv(argv[1]);
// }
// continue;
// }
if(strcmp(argv[0], "ls") == 0){
int pos = 0;
while(argv[pos]){
pos++;
}
argv[pos++] = (char*)"--color=auto";
argv[pos] = NULL;//比较安全的做法
}
//version 1
pid_t id = fork();
assert(id >= 0);
(void)id;
if(id == 0){
//child
// execvp(argv[0], argv);
execvp(argv[0], argv);
exit(1);
}
int status = 0;
pid_t ret_id = waitpid(id, &status, 0);
if(ret_id > 0){
last_exit = WEXITSTATUS(status);
}
}
}