进程控制
一、进程创建
1. 再识fork
在初始进程这篇博客中,浅谈了fork这个函数,在进程地址空间这篇中,也解释了一些关于fork返回值的问题。在这里再认识一下fork函数。
#include <unistd.h>
pid_t fork(void);
//返回值:如果成功,给父进程返回子进程PID,给子进程返回0,如果失败返回-1
来一段代码测试:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdlib.h>
int main()
{
printf("Before:pid id %d\n", getpid());
pid_t pid = fork();
if(pid < 0)
{
perror("fork");
exit(-1);
}
printf("After:pid is %d, fork return %d\n", getpid(), pid);
sleep(1);
return 0;
}
上述代码运行结果:
fork运行的逻辑结构:
fork常规用法:
- 父进程希望复制自己,使父子进程同时执行不同的代码段。eg:父进程等待客户端请求,生成子进程处理请求。
- 一个进程要执行一个不同的程序。(进程替换,后面讲)
2. 写时拷贝
在进程地址空间这篇博客中也对写时拷贝进行了说明
OS为了提高效率,在创建子进程时会使用写时拷贝。本质就是按需申请,不会浪费系统资源
通过触发页表的可读权限,后续判断写入错误,发生写时拷贝,并更改权限
二、进程终止
前言——查看进程退出码
程序在执行完,无论正不正确都有一个退出码,可以查看
先实践一下:
#include <stdio.h>
int main()
{
printf("hello Linux!\n");
return 0;
}
查看退出码:
退出码是0,程序正常运行结束
1. 退出情况
第一种情况就不再实验
正常运行,结果不正确
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
int main()
{
int *p = (int*)malloc(1000*1000*1000*4);
if(p == NULL)
{
printf("error msg:%s\n", strerror(errno));
exit(errno);
}
return 0;
}
查看退出码:
退出码是12,可见结果不正确。我们也打印了错误信息Cannot allocate memory
异常退出
#include <stdio.h>
int main()
{
int a = 1, b = 0;
a = a / b;
return 0;
}
查看运行结果:
注: 第二次执行echo $?
发现结果是0。原因是上一次echo $?
也是程序,并且是正常运行得到正常结果
在前面也说程序没有执行完就结束,是因为OS对进程发送了异常的信号。
测试一下:
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
while(1)
{
printf("PID: %d\n", getpid());
sleep(1);
}
return 0;
}
测试结果:
对3330进程发送了11号信号,所以左边的会话弹出段错误,并且程序结束。
2. 退出码
C语言提供了两个函数和一个全局变量显示程序的错误信息。
strerror和errno
代码:
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
int main()
{
int *p = (int*)malloc(1000*1000*1000*4);
if(p == NULL)
{
printf("error msg:%s\n", strerror(errno));
exit(errno);
}
return 0;
}
运行结果:
系统中设置的错误码信息
一共133个非常多
测试代码:
#include <stdio.h>
#include <string.h>
int main()
{
for(int i = 0; i < 140; i++)
{
printf("strerror[%d] -> msg:%s\n", i, strerror(i));
}
return 0;
}
运行结果: (注:没有全部截下来,太长了)
perror
测试代码:
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
int main()
{
int *p = (int*)malloc(4*1000*1000*1000);
if(p == NULL)
{
perror("malloc error");
exit(errno);
}
return 0;
}
测试结果:
直接打印出错误信息。
可以这样理解:strerror+errno+printf == perror
异常信息
查看所有信号:
信号部分,再进行介绍
3. 退出方法
exit和_exit
#include <stdio.h>
#include <stdlib.h>
int main()
{
printf("hello Linux");
exit(0);
return 0;
}
代码运行结果:
#include <stdio.h>
#include <unistd.h>
int main()
{
printf("hello Linux");
_exit(0);
return 0;
}
代码运行结果:
结论:通过结果发现exit刷新了缓冲区,_exit没有刷新缓冲区
exit和_exit的底层关系:
三、进程等待
1. 解决等待的三个问题
2. 系统调用
wait
头文件:
1. #include <sys/types.h>
2. #include <sys/wait.h>
函数声明:
pid_t wait(int *status);
参数:输出型参数,执行结束status会带出进程的退出状态(包括错误信息和异常信息)
返回值:成功返回所等待进程的pid,失败返回-1
参数为NULL
eg1:(简单使用)
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <sys/types.h>
int main()
{
pid_t id = fork();
if(id == 0)
{
//child
printf("I am child, pid: %d\n", getpid());
}
else if(id < 0)
{
perror("fork:");
exit(errno);
}
else
{
sleep(5);
pid_t ID = wait(NULL);
if(ID == id)
{
printf("等待成功!\n");
}
else
{
printf("等待失败!\n");
}
}
sleep(3);
return 0;
}
预期结果:创建子进程,子进程执行完代码先等待三秒,然后父进程等待五秒,其中差额的两秒子进程进入僵尸状态,然后父进程休眠完之后等待成功,再等待三秒结束进程。
实验结果: (符合预期结果)
eg2:(多个子进程进行等待)
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <errno.h>
#include <stdlib.h>
#define N 3
void RunChild()
{
int cnt = 5;
while(cnt--)
{
printf("I am Child Process, pid: %d, ppid: %d\n", getpid(), getppid());
sleep(1);
}
}
int main()
{
for(int i = 0; i < N; i++)
{
pid_t id = fork();
if(id == 0)
{
RunChild();
exit(i);
}
printf("create child process:%d success\n", id); //这句话只有父进程才会执行
}
sleep(10);
//开始等待
for(int i = 0; i < N; i++)
{
pid_t id = wait(NULL);
if(id > 0)
{
printf("回收进程:%d->%d\n", i, id);
}
}
sleep(5);
return 0;
}
实验结果:在回收子进程的过程中,等到谁就释放谁
使用status参数
退出信息位分布:(低16位)
来两个小demo测试一下:
正常终止:
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
int main()
{
pid_t id = fork();
if(id == 0)
{
//child
printf("I am child, pid: %d\n", getpid());
exit(11);
}
else if(id > 0)
{
//father
int status = 0;
pid_t ID = wait(&status);
if(ID == id)
{
printf("子进程的退出信息:%d\n", status);
printf("等待成功!\n");
}
else
{
printf("等待失败!\n");
}
}
else
{
perror("fork");
exit(11);
}
return 0;
}
运行结果:
异常终止:
#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
int main()
{
pid_t id = fork();
if(id == 0)
{
//child
printf("I am child, pid:%d\n", getpid());
//出现异常
int *p = NULL;
*p = 0;
}
else if(id > 0)
{
int status = 0;
pid_t ID = wait(&status);
if(ID == id)
{
printf("子进程的退出信息:%d\n", status);
printf("等待成功!\n");
}
else
{
printf("等待失败!\n");
}
}
else
{
perror("fork:");
exit(errno);
}
return 0;
}
运行结果:
小结
根据上文的测试:大致可以观察到测试结果和退出信息的位分布结果是一致的。
根据上面的小结,再做一些小实验:
#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
int main()
{
pid_t id = fork();
if(id == 0)
{
//child
printf("I am child, pid:%d\n", getpid());
//出现异常
int *p = NULL;
*p = 0;
}
else if(id > 0)
{
int status = 0;
pid_t ID = wait(&status);
if(ID == id)
{
if(WIFEXITED(status))
{
//printf("正常退出,退出状态:%d\n", WEXITSTATUS(status)); //和下面一条语句结果一样
printf("正常退出,退出状态:%d\n", (status >> 8) & 0xFF);
}
if(WIFSIGNALED(status))
{
//printf("异常退出,终止信号:%d\n", WTERMSIG(status)); //和下面一条语句结果一样
printf("异常退出,终止信号:%d\n", status & 0x7F);
}
printf("等待成功!\n");
}
else
{
printf("等待失败!\n");
}
}
else
{
perror("fork:");
exit(errno);
}
return 0;
}
运行结果:
注: 这个测试,只是测试了异常终止的代码,正常结束的程序就不再赘述了。
waitpid
头文件:
1. #include <sys/types.h>
2. #include <sys/wait.h>
函数声明:
pid_t waitpid(pid_t pid, int *status, int options);
参数:
pid:
1. pid = -1,等待任意一个子进程。与wait的作用一样
2. pid > 0。等待与pid相等的子进程
status:(该参数在wait部分已经详细介绍)
WIFEXITED:正常终止的子进程,则为真。(查看进程是否正常退出)
WEXITSTATUS:WEXITSTATUS不为0,提取子进程退出码。(查看进程的退出码)
options:
WNOHANG:若pid指定的子进程没有结束,则waitpid()函数返回0,不进行等待。若正常结束,则返回该子进程pid
0:进行阻塞等待
WUNTRACED:如果子进程进入暂停状态就立刻返回。
返回值:
1. 当正常返回的时候waitpid返回的是收集到子进程的PID
2. 如果设置了第三个参数WNOHANG,而调用waitpid发现没有已退出的子进程可收集,则返回0
3. 调用出错,则返回-1,errno也会被设置
注: 在这里就不再对waitpid进行实验,在下一节内容顺带实验
3. 阻塞和非阻塞等待
阻塞等待:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <errno.h>
int main()
{
pid_t pid = fork();
if(pid < 0)
{
perror("fork");
exit(errno);
}
else if (pid == 0)
{
//child
printf("child Running, pid:%d\n", getpid());
sleep(5);
exit(140);
}
else
{
int status = 0;
pid_t ret = waitpid(-1, &status, 0); //第一个参数代表任意子进程都可以再次等待,第三个参数就是阻塞等待
printf("wait......5s..\n");
if(WIFEXITED(status) && ret == pid)
{
printf("等待成功,ret_code:%d\n", WEXITSTATUS(status));
}
else
{
printf("等待失败!\n");
exit(errno);
}
}
return 0;
}
运行结果:
非阻塞等待:
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <errno.h>
#include <unistd.h>
#define TASK_NUM 10
typedef void(*task_t)(); //定义函数指针
task_t tasks[TASK_NUM]; //函数指针数组
void task1()
{
printf("执行打印日志的任务,pid:%d\n", getpid());
//...
}
void task2()
{
printf("检测网络健康状态的任务,pid:%d\n", getpid());
//...
}
void task3()
{
printf("绘制图形界面的任务,pid:%d\n", getpid());
}
int AddTask(task_t t);
//管理任务
void InitTask()
{
for(int i = 0; i < TASK_NUM; i++)
{
tasks[i] = NULL;
}
AddTask(task1);
AddTask(task2);
AddTask(task3);
}
int AddTask(task_t t)
{
int pos = 0;
for(; pos < TASK_NUM; pos++) //寻找空位置
{
if(!tasks[pos])
break;
}
//再判断边界
if(pos == TASK_NUM)
return -1;
//插入任务
tasks[pos] = t;
return 0;
}
//执行任务
void ExecuteTask()
{
for(int i = 0; i < TASK_NUM; i++)
{
if(!tasks[i]) //遍历指针数组
continue;
tasks[i]();
}
}
int main()
{
pid_t id = fork();
if(id < 0)
{
perror("fork");
exit(errno);
}
else if(id == 0)
{
//child
int cnt = 5;
while(cnt--)
{
printf("I am child, pid:%d, ppid:%d, cnt:%d\n", getpid(), getppid(), cnt);
sleep(1);
}
exit(11);
}
else
{
int status = 0;
InitTask();
while(1) //非阻塞轮询
{
pid_t ret = waitpid(id, &status, WNOHANG); //阻塞只需要该WNOHANG为0即可
if(ret > 0)
{
if(WIFEXITED(status))
{
printf("代码正常跑完,退出码:%d\n", WEXITSTATUS(status));
}
else
{
printf("代码异常!\n");
}
break;
}
else if(ret < 0)
{
printf("wait failed!\n");
break;
}
else //轮询,处理别的任务
{
ExecuteTask();
sleep(1);
}
}
}
return 0;
}
代码运行结果:
4. 进程等待的原理
四、进程替换
1. 概念
2. exec函数族
exec系列函数:
头文件:#include <unistd.h>
返回值:
1. 函数调用成功,则加载新的程序,开始执行代码,不返回
2. 替换失败,返回-1.按照原先的代码继续运行。错误码被设置
①execl
函数声明:
int execl(const char *path, const char *arg, ...);
参数:
1. path:指的是所要打开文件具体的路径
2. arg:所要打开的文件名
3. ...:可变参数列表,传的是具体选项,且以NULL结尾
测试:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main()
{
pid_t id = fork();
if(id == 0)
{
//进行进程替换
int ret = execl("/usr/bin/ls", "ls", "-l", NULL);
//int ret = execl("ls", "ls", "-l", NULL); //这个填的路径是相对路径,就是相对当前路径来说
//执行ls -l命令
if(ret == -1)
{
printf("进程替换失败!\n");
}
}
return 0;
}
测试结果: (进程替换成功)
②execlp
函数声明:
int execlp(const char *file, const char *arg, ...);
参数:
1. file:指的是所要打开文件的路径,若不加路径,可以在当前路径和PATH环境变量下的路径寻找
2. arg:所要打开的文件名
3. ...:可变参数列表,传的是具体选项,且以NULL结尾
测试:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main()
{
pid_t id = fork();
if(id == 0)
{
//进行进程替换
int ret = execlp("ls", "ls", "-l", NULL);
//执行ls -l命令
if(ret == -1)
{
printf("进程替换失败!\n");
}
}
return 0;
}
测试结果:
③execle
函数声明:
int execle(const char *path, const char *arg, ..., char *const envp[]);
参数:
1. path:指的是所要打开文件具体的路径
2. arg:所要打开的文件名
3. ...:可变参数列表,传的是具体选项,且以NULL结尾
4. envp:新的环境变量数组,即新执行程序的环境变量
测试:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
printf("before exec: USER=%s, HOME=%s\n", getenv("USER"), getenv("HOME"));
char *const env[] = {(char *const)"USER=KKK", (char *const)"HOME=hhhh", NULL};
int ret = execle("./process", "process", NULL, env);
if(ret == -1)
{
perror("execle");
exit(11);
}
printf("After\n");
return 0;
}
替换的程序:
#include <stdio.h>
#include <cstdlib>
#include <unistd.h>
int main()
{
printf("USER=%s\n", getenv("USER"));
printf("HOME=%s\n", getenv("HOME"));
return 0;
}
测试结果:
注意:因为一次编译了两个程序,所以我们在编译makefile时,要借用为目标,让其推到自己编译好两个程序
makefile文件:
.PHONY:ALL
ALL:test process
process:process.cpp
g++ -o $@ $^
test:test.c
gcc -o $@ $^ -std=c99
.PHONY:clean
clean:
rm -f test process
④execv
函数声明:
int execv(const char *path, char *const argv[]);
参数:
1. path:替换程序的路径
2. argv[]:保存的是参数列表,将可执行文件和参数保存到字符串数组中,最后以NULL结尾。
测试:
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
char *const argv[] = { (char *const)"ls", (char *const)"-a", (char *const)"-l", NULL };
pid_t id = fork();
if(id == 0)
{
int ret = execv("/usr/bin/ls", argv);
if(ret == -1)
{
printf("进程替换失败!\n");
}
}
return 0;
}
测试结果:
⑤execvp
函数声明:
int execvp(const char *file, char *const argv[]);
参数:
1. file:所要打开的文件路径(绝对和相对路径)。也可以在PATH环境变量下寻找
2. argv:保存的是参数列表,将可执行文件和参数保存到字符串数组中,最后以NULL结尾。
测试:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#define ARR_NUM 4
int main()
{
char *const argv[ARR_NUM] = { (char *const)"ls", (char *const)"-a", (char *const)"-l", NULL};
pid_t id = fork();
if(id == 0)
{
int ret = execvp("ls", argv);
if(ret == -1)
{
printf("进程替换失败!\n");
}
}
return 0;
}
测试结果:
⑥execvpe
函数声明:
int execvpe(const char *file, char *const argv[], char *const envp[]);
参数:
1. file:指的是所要打开文件的路径,也可在环境变量PATH下寻找
2. argv:指的是所要打开文件的名及选项
3. envp:要传入的环境变量数组
测试代码:(test进程,其中子进程替换成process进程)
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#define ARR_NUM 3
extern char** environ;
int main()
{
char *const argv[ARR_NUM] = { (char *const)"process", NULL };
char *str =(char *)"MY_VAL=449220104";
putenv(str);
pid_t id = fork();
if(id == 0)
{
int ret = execvpe("./process", argv, environ);
if(ret == -1)
{
printf("进程替换失败!\n");
}
}
return 0;
}
process:
#include <iostream>
using namespace std;
int main(int argc, char *argv[], char *env[])
{
for(int i = 0; env[i]; i++)
{
cout << "env[" << i << "]: " << env[i] << endl;
}
return 0;
}
测试结果比较长,这里就不展示了。前面再介绍execle函数时,也说了如何编译多个程序
小结
上面我们介绍了exec系列的六种函数接口。这六个函数接口都包含在库文件中,而在这六个接口的底层,无疑调用了系统调用,这个系统调用接口就是execve。
函数接口 | 参数格式 | 是否带路径 | 是否使用当前环境变量 |
---|---|---|---|
execl | 列表 | 不是 | 是 |
execlp | 列表 | 是 | 是 |
execle | 列表 | 不是 | 需要自己组装环境变量 |
execv | 数组 | 不是 | 是 |
execvp | 数组 | 是 | 是 |
execvpe | 数组 | 是 | 需要自己组装环境变量 |
execve(系统调用) | 数组 | 不是 | 需要自己组装环境变量 |
代码:
#include <unistd.h>
int main()
{
char *const argv[] = { "ps", "-axj", NULL };
char *const envp[] = { "PATH=/bin:/usr/bin", "TERM=console", NULL };
execl("/bin/ps", "ps", "-axj", NULL);
//带p的,可以使用环境变量PATH
execlp("ps", "ps", "-axj", NULL);
//带e的,需要自己组装环境变量
execle("ps", "ps", "-axj", NULL, envp);
execv("/bin/ps", argv);
//带p的,可以使用环境变量PATH
execvp("ps", argv);
//带e的,需要自己组装环境变量
execve("/bin/ps", argv, envp);
//带p和e的
execvpe("ps", argv, envp);
return 0;
}
六个函数的关系图:(去掉了GNU扩展的那个函数)
3. 系统调用——execve
函数声明:
int execve(const char *path, char *const argv[], char *const envp[]);
参数:
1. path:替换程序的路径
2. argv:保存的是参数列表,可执行文件和参数保存到字符串数组中,以NULL结尾
3. envp:新的环境变量数组
测试:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main() {
char *const argv[] = { "ls", "-l", NULL };
char *const envp[] = { "PATH=/bin", NULL };
int ret = execve("/bin/ls", argv, envp);
if (ret == -1) {
perror("execve");
exit(EXIT_FAILURE);
}
return 0;
}
测试结果:
4. 总结
小知识:讲这个的原因,是因为说到了C代码可以替换C++程序,所以进行扩展
test.sh --> 全称.Shell --> Shell脚本
Shell脚本就是把Linux命令放到一个文件
开头-->#!(shebang-->用于指定脚本文件的解释器。作用就是告诉OS用那个解释器执行脚本文件) 所以要紧跟脚本语言对应的解释器。
-----------------------------------
test.py:
#!/usr/bin/python3
print("hello Python!")
-----------------------------------
test.sh:
#!/usr/bin/bash
function myfun()
{
cnt=1
while [ $cnt -le 10]
do
echo "hello $cnt"
let cnt++
done
}
echo "hello 1"
ls -a -l
myfun
-----------------------------------
替换上面的程序:
execl("/usr/bin/bash", "bash", "test.sh", NULL);
execl("/usr/bin/python3", "python3", "test.py", NULL);