【Linux】4.0进程控制
文章目录
- Linux进程退出
- Linux进程等待
- wait( )函数
- waitpid( )函数
- 进程程序替换
- exec*函数系列
- Makefile创建多个程序
- mini_shell
- 内建命令和第三方命令
Linux进程退出
- 代码运行完毕,结果正确
- 代码运行完毕,结果错误
- 代码异常终止
进程退出码:传递进程退出时的状态信息
获取指令:echo $?
#include <stdio.h>
int main()
{
printf("hello world\n");
return 123; //main函数return的值就是进程的退出码
}
[clx@VM-20-6-centos process_control]$ make
gcc -o my_process my_process.c -std=c99
[clx@VM-20-6-centos process_control]$ ./my_process
hello world
[clx@VM-20-6-centos process_control]$ echo $?
123
写一个小程序,当程序运行起来时就成为一个进程,当程序运行结束进程就会将其的退出码传递给它的父进程,我们可以通过指令 echo $? 来查看最近的一个进程结束的退出码
作用: 进程退出码一般来说0表示success ,非零表示failed。非零时有很多程序退出码,不同退出码代表不同的信息来反映进程退出时出错的一种可能性
exit()和 _exit()
exit() :在任何地方调用,都代表终止进程,参数是退出码!
_exit():强制终止进程,不进行进程的后续收尾工作(执行用户定义的清理函数,刷新用户级缓冲区)
进程退出,OS做了什么?
系统层面少了一个进程, 需要free PCB(进程控制块) , free mm_struct(进程地址空间), free 页表和各种映射关系, 还有代码和数据
Linux进程等待
为何需要进程等待?
1.通过获取子进程的推出信息,才能得到子进程的执行结果
2.可以保证:时序问题,子进程先退出,父进程后退出
3.进程退出时若退出信息未被接受,会进入僵尸状态,造成内存泄漏问题,需要通过父进程wait接收子进程的退出信息,然后释放该子进程占用的资
wait( )函数
pid_t wait(int *status); //返回子进程的pid
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
pid_t id = fork(); //创建一个运行五秒的子进程
if (id == 0)
{
//child
int count = 5;
while (count){
printf("I am child. my pid = %d, count = %d\n", getpid(), count);
sleep(1);
count--;
}
exit (10);
}
//父进程先睡十秒
sleep(10);
pid_t ret = wait(NULL);
printf("I'm father\n");
if (ret){
printf("Father wait success\n");
}
else{
printf("wait failed\n");
}
sleep(5); //再睡五秒钟
return 0;
}
以上这个二十秒的小程序有三个阶段。
第一个阶段(前五秒):父子都在运行,子进程在打印内容,父进程休眠中
第二个阶段(五到十秒):子进程已经执行完毕,成为僵尸进程,父进程还在执行sleep指令
第三个阶段(十秒到二十秒):十秒开始父进程接收子进程信息,子进程退出成功,然后父进程继续沉睡十秒,这时候只有一个进程
设计脚本调出监控窗口看一下
脚本代码
while :; do ps ajx | head -1 && ps ajx | grep clx_proc |grep -v grep; sleep 1; echo "############################################################"; done
//前五秒
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
13685 20099 20099 13685 pts/4 20099 S+ 1001 0:00 ./clx_proc
20099 20100 20099 13685 pts/4 20099 S+ 1001 0:00 ./clx_proc
############################################################
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
13685 20099 20099 13685 pts/4 20099 S+ 1001 0:00 ./clx_proc
20099 20100 20099 13685 pts/4 20099 S+ 1001 0:00 ./clx_proc
############################################################
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
13685 20099 20099 13685 pts/4 20099 S+ 1001 0:00 ./clx_proc
20099 20100 20099 13685 pts/4 20099 S+ 1001 0:00 ./clx_proc
############################################################
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
13685 20099 20099 13685 pts/4 20099 S+ 1001 0:00 ./clx_proc
20099 20100 20099 13685 pts/4 20099 S+ 1001 0:00 ./clx_proc
############################################################
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
13685 20099 20099 13685 pts/4 20099 S+ 1001 0:00 ./clx_proc
20099 20100 20099 13685 pts/4 20099 S+ 1001 0:00 ./clx_proc
############################################################
//第六秒,子进程进入僵尸状态
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
13685 20099 20099 13685 pts/4 20099 S+ 1001 0:00 ./clx_proc
20099 20100 20099 13685 pts/4 20099 Z+ 1001 0:00 [clx_proc] <defunct>
############################################################
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
13685 20099 20099 13685 pts/4 20099 S+ 1001 0:00 ./clx_proc
20099 20100 20099 13685 pts/4 20099 Z+ 1001 0:00 [clx_proc] <defunct>
############################################################
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
13685 20099 20099 13685 pts/4 20099 S+ 1001 0:00 ./clx_proc
20099 20100 20099 13685 pts/4 20099 Z+ 1001 0:00 [clx_proc] <defunct>
############################################################
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
13685 20099 20099 13685 pts/4 20099 S+ 1001 0:00 ./clx_proc
20099 20100 20099 13685 pts/4 20099 Z+ 1001 0:00 [clx_proc] <defunct>
############################################################
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
13685 20099 20099 13685 pts/4 20099 S+ 1001 0:00 ./clx_proc
20099 20100 20099 13685 pts/4 20099 Z+ 1001 0:00 [clx_proc] <defunct>
############################################################
//第十秒开始只有一个进程正在运行
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
13685 20099 20099 13685 pts/4 20099 S+ 1001 0:00 ./clx_proc
############################################################
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
13685 20099 20099 13685 pts/4 20099 S+ 1001 0:00 ./clx_proc
############################################################
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
13685 20099 20099 13685 pts/4 20099 S+ 1001 0:00 ./clx_proc
############################################################
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
13685 20099 20099 13685 pts/4 20099 S+ 1001 0:00 ./clx_proc
############################################################
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
13685 20099 20099 13685 pts/4 20099 S+ 1001 0:00 ./clx_proc
############################################################
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
############################################################
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
############################################################
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
############################################################
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
############################################################
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
^C
以上即是wait的功能介绍,wait可以使得父进程可以接收子进程退出信息,防止出现内存泄漏存
waitpid( )函数
pid_t waitpid(pid_t pid, int *status, int options);
参数介绍:
pid:接收子进程的pid,若输入-1,则可以wait任意一个子进程
status:状态信息
最后的7位代表收到的信号,倒数第八位是core dump标志,倒数第九位到第十六位则对应我们的退出码
int status = 0; //创建变量status
pid_t ret = waitpid(id, &status, 0); //用status的地址当作形参传给waitpid
printf("I'm father\n");
if (ret){
printf("Father wait success\n");
printf("child pid = %d, exit = %d signal = %d\n", ret, (status >> 8) & 0xFF, status& 0x7f);
//(status >> 8) & 0xFF 取次八位的值 退出码
//status & 0x7F 取最后七位的值 信号
}
为了更为遍历,C语言还提供了一组宏来帮助我们提取status的信息
WIFEXITED(status):进程正常终止返回真,若进程异常终止返回假
WEXITSTATUS(status):若WIFEXITEND为真,则提取进程退出的退出码
以下是两个宏的用法
int main()
{
pid_t id = fork();
if (id == 0)
{
//child
int count = 5;
while (count){
printf("I am child. my pid = %d, count = %d\n", getpid(), count);
sleep(1);
//int a = 100/ 0 ; //设计错误,这样代码就会收到信号造成异常退出
count--;
}
exit (10);
}
int status = 0;
pid_t ret = waitpid(id, &status, 0);
printf("I'm father\n");
if (ret){
if (WIFEXITED(status)) //若运行成功退出,则可以提取进程退出码
{
printf("exit code = %d\n", WEXITSTATUS(status));
}
//若异常退出说明进程收到信号异常终止,输出收到的信号
else {
printf("catch a singal = %d\n", status & 0x7f);
}
}
else{
printf("wait failed\n");
}
return 0;
}
options:默认行为
当输入0时,进行阻塞等待
当输入WNOHANG时,进行非阻塞等待,循环控制
阻塞的本质:其实是进程的PCB被放入等待队列,并将进程的状态设置成S状态
返回的本质:进程的PCB从等待队列中放出,被CPU进行调度
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
pid_t id = fork();
if (id == 0)
{
//child
int count = 5;
while (count){
printf("I am child. my pid = %d, count = %d\n", getpid(), count);
sleep(1);
count--;
}
exit (10);
}
int status = 0;
while (1)
{
pid_t ret = waitpid(id, &status, WNOHANG);
if (ret == 0)
{
//子进程还未退出,继续等待
printf("father do something\n");
sleep(2);
}
else if (ret > 0)
{
//子进程退出,获取退出信息
printf("father wait : %d success, status exit cond = %d, status exit singal = %d\n", ret, WEXITSTATUS(status), status & 0x7f);
break;
}
else
{
//waitpid本身有问题,返回-1 退出
printf("wait fail\n");
break;
}
}
输出的结果
[clx@VM-20-6-centos process_control]$ make
gcc -o clx_proc clx_proc.c -std=c99
[clx@VM-20-6-centos process_control]$ ./clx_proc.c
-bash: ./clx_proc.c: Permission denied
[clx@VM-20-6-centos process_control]$ ./clx_proc
father do something
I am child. my pid = 4874, count = 5
I am child. my pid = 4874, count = 4
father do something
I am child. my pid = 4874, count = 3
I am child. my pid = 4874, count = 2
father do something
I am child. my pid = 4874, count = 1
father wait : 4874 success, status exit cond = 10, status exit singal = 0
进程程序替换
概念:进程不变,仅仅替换当前进程的代码和数据
价值:想让子进程执行一个全新的程序
C/C++程序想要运行,则需要将代码和数据加载到内存中,即特定进程的上下文中,如何加载呢?
使用的就是加载器(exec* 程序替换函数)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
pid_t id = fork();
if (id == 0)
{
sleep(5);
printf("I'm a process, pid = %d\n", getpid());
execl("/usr/bin/ls", "ls", "-a", "-l", NULL);
printf("hello world\n");
printf("hello world\n");
printf("hello world\n");
printf("hello world\n");
exit(1);
}
while (1)
{
printf("I'm father\n");
sleep(1);
}
pid_t ret = waitpid(id, NULL, 0);
printf("wait success , child pid = %d", ret);
return 0;
}
运行结果
I'm father
I'm father
I'm father
I'm father
I'm father
I'm father
I'm a process, pid = 13236
total 28
drwxrwxr-x 2 clx clx 4096 Oct 17 17:30 .
drwxrwxr-x 3 clx clx 4096 Oct 17 10:02 ..
-rwxrwxr-x 1 clx clx 8664 Oct 17 17:30 clx_proc
-rw-rw-r-- 1 clx clx 2208 Oct 17 17:30 clx_proc.c
-rw-rw-r-- 1 clx clx 81 Oct 17 15:26 Makefile
I'm father
I'm father
I'm father
I'm father
I'm father
当子进程调用进程替换函数后,父进程不受影响,这是因为进程之间具有独立性。
当进程程序替换时,会修改代码区的代码,所以会进行写时拷贝
只要进程的程序替换成功,那么就不会执行后续代码,意味着exec函数,成功的时候并不需要返回值检验,因为只要exec函数返回了,那么就一定是调用失败了
int main()
{
printf("I'm a process, pid = %d\n", getpid());
execl("/usr/bin/llllls", "ls", "-a", "-l", NULL);
exit(1); //若程序走到这里一定是调用失败了,直接返回错误码表示错误
}
exec*函数系列
先以execl举例
//库函数
EXEC(3) Linux Programmer's Manual EXEC(3)
NAME
execl, execlp, execle, execv, execvp, execvpe - execute a file
SYNOPSIS
#include <unistd.h>
extern char **environ;
int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg,..., char * const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[],char *const envp[]);
//系统调用接口
EXECVE(2) Linux Programmer's Manual EXECVE(2)
NAME
execve - execute program
SYNOPSIS
#include <unistd.h>
int execve(const char *filename, char *const argv[],
char *const envp[]);
可以看到其实系统调用接口只有一个,但是C语言却提供了六个调用函数,C语言的调用接口本质上是对系统调用接口的一个封装,方便我们不同环境下使用进程程序替换
命名规律:
" l " : 可变列表参数一个个输入
" v ": 可变列表参数使用数组传入
" p ": 自动搜索环境变量PATH
" e ":表示自己维护环境变量
带 l 和带 v 的函数非常相近,只是一个直接传参,一个将参数放在数组中了
//以下是exec* 系列函数的调用
int main()
8 {
9 if (fork() == 0)
10 {
11 printf("I' m child, pid = %d\n", getpid());
12 //char* argv[] = {"ls", "-a", "-i", "-l", NULL}; //将可变参数列表放在数组中
13 //execvp("ls", argv); //带有p会直接到环境变量中查找指令
14 //execlp("ls", "ls", "-a", "-i", "-l", NULL);
15 //execv("/usr/bin/ls", argv);
16 //execl("/usr/bin/ls", "ls", "-a", "-i", "-l", NULL);
17 //execl("./my_exe", "my_exe", NULL);
18 //char* env[] = {"hello world", NULL};
19 //execle("./my_exe", "my_exe", NULL, env);
20 execl("/usr/bin/python3", "python", "test.py", NULL);
21 exit(1);
22 }
23 int status = 0;
24 pid_t ret = waitpid(-1, &status, 0);
25 if (ret > 0 && WIFEXITED(status))
26 {
27 printf("child exit code = %d, catch singal = %d\n", WEXITSTATUS(status), status & 0x7f);
28 printf("waitpid success\n");
29 }
30 return 0;
31 }
这里重点说一下execle函数
int main()
{
if (fork() == 0)
{
printf("I' m child, pid = %d\n", getpid());
char* env[] = {"env_val1 = hello world", "env_val2 = hello linux", NULL};
execle("./my_exe", "my_exe", NULL, env); //文件所在路径就在./中
exit(1);
}
int status = 0;
pid_t ret = waitpid(-1, &status, 0);
if (ret > 0 && WIFEXITED(status))
{
printf("child exit code = %d, catch singal = %d\n", WEXITSTATUS(status), status & 0x7f);
printf("waitpid success\n");
}
return 0;
}
//my_exe.c //用于打印环境变量
#include <stdio.h>
int main()
{
extern char** environ;
for (size_t i = 0; environ[i]; i++)
{
printf("%s\n", environ[i]);
}
return 0;
}
运行程序
[clx@VM-20-6-centos process_replace]$ ./my_test
I' m child, pid = 29917
env_val1 = hello world
env_val2 = hello linux
child exit code = 0, catch singal = 0
waitpid success
可以看到程序将我们自己传入的环境变量打印了出来,于带p的函数不同,其使用的是我们自己传入的环境变量
Makefile创建多个程序
在exec*系列函数的execle实例中我们调用了自己的my_exe可执行程序,那么如何使用Makefile一次性创建多个可执行程序呢?
.PHONY:all //使用伪目标all
all:my_exe my_test
my_exe:my_exe.c
gcc -o $@ $^ -std=c99
my_test:my_test.c
gcc -o $@ $^ -std=c99
.PHONY:clean
clean:
rm -f my_test my_exe
我们可以使用伪目标all,生成all需要两个可执行程序,所以Makefile就会自动去生成两个可执行程序。如果想要生成更多的可执行文件,只需要将文件名写在all后面就行了
mini_shell
为了能更深刻的理解进程程序替换,我们写一个迷你版的shell脚本来加深印象
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <stdlib.h>
#include <sys/types.h>
#include <string.h>
#define MAX_LEN 128//指令的最大长度
#define CMD_NUM 64//指令可包含最大参数个数
int main()
{
char command[MAX_LEN] = {0};
while (1)
{
char* argv[CMD_NUM] = {NULL};
command[0] = 0;
//1.打印提示符
printf("[clx@VM-20-6-centos my_shell]$");
fflush(stdout); //刷新缓冲区
//2.接收命令行
fgets(command, MAX_LEN, stdin);
command[strlen(command) - 1] = 0; //"ls -a\n" \n的下标为5 字符串长度为6
//3.解析命令行
const char* sep = " ";
argv[0] = strtok(command, sep);
size_t i = 1;
while (argv[i] = strtok(NULL, sep)) {i++;}
//4.检测命令是否是shell本身执行的,内建命令
if (strcmp(argv[0], "cd") == 0)
{
if (argv[1] != NULL)
{
chdir(argv[1]);
continue;
}
}
//5.创建子进程执行第三方命令
if (fork() == 0)
{
//child
execvp(argv[0], argv);
exit(1);
}
//父进程进行夯等待
waitpid(-1, NULL, 0);
}
return 0;
}
整个程序思路很简单,但是其中调用的几个函数需要注意
1.fflush() :刷新缓冲区不换行,若直接使用\n会导致换行输入命令,与实际的shell脚本不符合
2.fgets() :获取一行字符串,第一个参数接收字符串,第二个是字符串的最大长度,第三个表示输入流
3.strtok:这个实在C语言字符操作里有的,第一个参数是一个字符串,第二个参数是分割符。内部有个static全局变量来记录分割位置,第二次操作第一个参数只需要传NULL就可以继续向后切割了
若还不清楚可以调用man手册查阅文档
内建命令和第三方命令
内建命令就是进程改变自己的命令:如cd …代表进程需要修改自己的所在路径。若使用子进程进行调用,因为进程相互之间具有独立性,则将修改子进程的所在路径,父进程路径不变
第三方命令:与当前进程无关,并不需要修改当前进程的数据的命令,大多放置在usr/bin目录中,可以自行查看