【Linux】-----进程控制
目录
一、进程终止
是什么?
终止的三种情况
终止的方式
①return 退出
②调用exit函数(库函数)
③调用_exit函数(系统调用)
二、进程等待
为什么要等?
等待方式
wait
waitpid
获取子进程的status
方式一:位操作(麻烦)
方式二:宏(常用)
非阻塞轮询访问
三、进程替换
什么是?
怎么样?
①execl
②execlp
③execle
④execv
⑤execvp
⑥execvpe
一、进程终止
是什么?
终止实际上是在释放曾经代码和数据所占据的空间,同时也是在释放内核数据结构
终止的三种情况
- 代码正常跑完,结果正确
- 代码正常跑完,结果不正确
- 代码出现异常,提前退出(进程崩溃)
对于前面的两种情况,可以通过退出码来判断
- 退出码为0,结果正确
- 退出码非0,结果不正确
非0之外的值,一方面是为了表示错误,另外一方面是为了告知用户错误的原因。实际上错误的原因在C语言中可以通过strerror函数获取对应的错误信息。里面记录了134个错误信息,除外的都是未知错误。
查看最近一次可执行程序的退出码指令:
echo $?
以ls命令演示
正常运行的退出码
不正常的退出码
对于第三种情况,进程崩溃,因为操作系统会发现你的进程做了不该做的事情(比如野指针等),操作系统会立马杀了进程,会给用户发出退出信号。所以要看具体是什么异常只需要看退出信号即可!此时的退出码没有意义了!
上述的异常为段错误,OS会立刻杀死进程。 实际上这相当于kill -11的指令
小总结:
衡量一个进程的退出,只需要两个关键字:退出码+退出信号
1.先确定是否异常
2.不是异常,说明代码一定能跑完,此时的看退出码就行!
终止的方式
①return 退出
return 0 表示成功,非0表示失败,原因在退出码。
在main函数中return,代表该进程结束!
在非main函数中return,代表当前函数结束!
②调用exit函数(库函数)
- 这是C库函数 ,在进程的任意地方调用,都表示进程的退出!需要注意在进程退出时,该函数会冲刷缓冲区的内容!
#include <unistd.h>
#include <sys/types.h>
#include <string.h>
#include <stdlib.h>
int main()
{
printf("i am a process\n");
sleep(1);
exit(3);
return 0;
}
③调用_exit函数(系统调用)
- 这个函数是系统调用,但是进程退出时,并不会冲刷缓冲区,会直接释放掉所有资源。
- 这个函数也是exit函数的底层。
- 使用和exit类似
二、进程等待
为什么要等?
- 解决子进程的僵尸问题,僵尸问题不解决,会造成内存泄漏的问题
- 其次,僵尸进程刀枪不入,kill命令无法终止,因为无法杀死一个已经死去的进程
- 另外,父进程需要知道给子进程的任务的完成情况。即退出信息
- 所以,父进程需要通过等待的方式,回收子进程的资源,并获取退出信息
等待方式
-
wait
系统级别的函数调用
参数:
这里的参数为输出型参数,能够获取子进程的退出信息(退出码+退出信号),不关心可以设置为NULL!
返回值:
- >0,等待成功,返回等待子进程的pid
- =-1,等待失败
函数特点:
如果子进程一直不退,父进程实际上会一直进行阻塞等待,本质就是父进程的PCB会被链入到子进程的等待队列里面,等待子进程就绪,在此期间,父进程啥也干不了,直到子进程退出才继续往下执行程序!
示例:
让子进程跑5s后,退出
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <string.h>
#include <stdlib.h>
#include <sys/wait.h>
//子进程跑5s退出
void ChildRun()
{
int cnt=5;
while(cnt)
{
printf("Childprocess runing now,id: %d,pid: %d,cnt:%d\n",getpid(),getppid(),cnt);
sleep(1);
cnt--;
}
}
int main()
{
printf("i am father process,id: %d,ppid: %d\n",getpid(),getppid());
pid_t id=fork();
if(id==0)
{
ChildRun();
printf("child quit.....\n");
exit(1);
}
sleep(7);
pid_t rid=wait(NULL);
if(rid > 0)
{
printf("wait sucessful..\n");
}
printf("father quit.....\n");
sleep(2);
return 0;
}
可以看到5s后,子进程的状态变为了僵尸,而后由于父进程在等待,子进程退出后就被在等待的父进程回收了!
-
waitpid
返回值pid_t:
- >0,等待成功,返回的是回收子进程的id
- <0,等待失败
- =0,检测到子进程,但是没有完成任务,父进程需要下一次重复等待(非阻塞等待时判断)
参数:
①pid
- -1,等待任一子进程退出,功能和wait一样。
- =某子进程id,等待进程为id的进程退出。
②status
和wait一样,可以查看退出信息,不关心可以设置为NULL
③options
设置父进程的等待方式:
0:阻塞等待
!0:非阻塞等待(一般设置为宏WNOHANG)
阻塞和非阻塞的区别:
- 父进程处于阻塞状态时,什么事情都做不了,必须等待子进程退出,回收子进程后才能去完成自己的事情,工作效率低
- 若处于非阻塞状态,在等待子进程退出的期间,可以去完成其他的任务,工作效率高,这种方式只需父进程每隔一段时间去检测子进程的状态,退出就回收,没退出就继续自己的任务,也称为非阻塞轮询访问!
举例:
pid_t rid=waitpid(-1,NULL,0);//这一句等同于wait(NULL)
//pid_t rid=waitpid(-1,NULL,WNOHANG);//非阻塞等待
if(rid > 0)
{
printf("wait sucessful..\n");
}
printf("father quit.....\n");
从上面两种等待方式可以看出,waitpid可选择的选项比wait要多,所以实际用的比较多!
获取子进程的status
方式一:位操作(麻烦)
上面说过wait和waitpid都有一个参数status,用于查看子进程的退出状态信息,是一个输出型参数,若不关心可以设置为NULL!若想要查看可以采用如下方式:
int status=0;
pid_t rid=waitpid(id,&status,0);
//wait(&status)
printf("father quit..status is:%d\n
child exit code:%d,child exit signal:%d\n",status,(status>>8)&0xFF,status&0x7F);
可以看到,上述的退出信息status的值为256,实际上这包含了两个部分退出码+退出信号。一个int有32个位,status只使用低16位来存储退出信息,高8位为退出码,低7位表示退出信号,还有1位是core dump位(不理)
进程正常退出:使用高8位来表示退出码,低8位全为0
异常退出:高8位不使用,低7位存储退出信号
上述例子,子进程退出码是1,正常退出,退出信号为0,所以二进制序列为00000001 00000000,即256!
所以根据这一原理,要想明确的获取子进程的退出码和退出信号,方式一可以采用移位操作。获取退出码:(status>>8)&0xFF,退出信号:status&0x7F
方式二:宏(常用)
WIFEXITED(status):查看是否正常退出
WEXITSTATUS(status):提取正常退出进程的退出码
示例:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <string.h>
#include <stdlib.h>
#include <sys/wait.h>
//子进程跑5s退出
void ChildRun()
{
int cnt=5;
while(cnt)
{
printf("Childprocess runing now,id: %d,pid: %d,cnt:%d\n",getpid(),getppid(),cnt);
sleep(1);
cnt--;
}
}
int main()
{
printf("i am father process,id: %d,ppid: %d\n",getpid(),getppid());
pid_t id=fork();
if(id==0)
{
ChildRun();
printf("child quit.....\n");
exit(1);//正常退出码为1
}
sleep(7);
int status=0;//获取退出信息
pid_t rid=waitpid(id,&status,0);
if(rid > 0)
{
//正常退出,只关注退出码就行
if(WIFEXITED(status))
{
printf("child quit sucess,child exit code: %d\n",WEXITSTATUS(status));
}
else
{
printf("child quit unnormal")
}
}
printf("father quit.....\n");
sleep(2);
return 0;
}
一句话:等待是必须的,但是获取退出信息,看情况而定 !!
非阻塞轮询访问
非阻塞轮询=非阻塞等待+循环!
WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID。
int status=0;//获取退出信息
pid_t rid=waitpid(id,&status,WNOHANG);//非阻塞等待
示例:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <string.h>
#include <stdlib.h>
#include <sys/wait.h>
//子进程跑5s退出
void ChildRun()
{
int cnt=5;
while(cnt)
{
printf("Childprocess runing now,id: %d,pid: %d,cnt:%d\n",getpid(),getppid(),cnt);
sleep(1);
cnt--;
}
}
int main()
{
printf("i am father process,id: %d,ppid: %d\n",getpid(),getppid());
pid_t id=fork();
if(id==0)
{
ChildRun();
printf("child quit.....\n");
exit(1);//正常退出码为1
}
while(1)
{
int status=0;//获取退出信息
pid_t rid=waitpid(id,&status,WNOHANG);//非阻塞等待
if(rid==0)
{
usleep(100000);
printf("child is running now,check next time\n");
printf("DoOtherting()\n");
}
else if(rid > 0)
{
//正常退出,只关注退出码就行
if(WIFEXITED(status))
{
printf("child quit sucess,child exit code: %d\n",WEXITSTATUS(status));
}
else
{
printf("child quit unnormal")
}
break;
}
}
printf("father quit.....\n");
sleep(1);
return 0;
}
三、进程替换
什么是?
- 进程的替换本质就是将当前程序的代码+数据替换成新程序的代码和数据
- 新程序的代码+数据也会被加载到内存中,通过exec*系列的替换函数加载替换
- 替换的过程没有创建新进程,替换前后的pid都没有改变,只是在用老程序的壳子执行新程序的代码
怎么样?
- exec*系列函数能完成进程的替换工作,头文件#include <unistd.h>
- 有返回值,但是不用关心,只要替换成功,就不会向后继续执行;反之,程序会继续向下执行
①execl
int execl(const char *path, const char *arg, ...);
path:你想替换的程序路径
*arg:参数列表,你想怎么执行程序(命令行参数怎么写就怎么写,最后以NULL结尾)
...:表示可变参数
示例:
#include <stdio.h>
#include <unistd.h>
int main()
{
printf("process begin....\n");
execl("/usr/bin/ls","ls","-l","-a",NULL);
printf("process end...\n");
return 0;
}
上述代码表示以ls -l -a的方式执行/usr/bin/ls程序
可以看到替换成功后,程序后续代码就不执行了,因为已经替换了!
②execlp
p:查找这个程序,系统会自动在环境变量中找
int execlp(const char *file, const char *arg, ...);
和上面的不同在于:此函数无需带路径,直接传文件名即可!
#include <stdio.h>
#include <unistd.h>
int main()
{
printf("process begin....\n");
execlp("ls","ls","-l","-a","--color",NULL);
printf("process end...\n");
return 0;
}
③execle
e:代表环境变量
能将环境变量导入要执行的程序中!
int execle(const char *path, const char *arg,
..., char * const envp[]);
示例:
#include <stdio.h>
#include <unistd.h>
int main()
{
extern char**environ;//用父进程的环境变量,也可以自定义
printf("process begin....\n");
execle("/usr/bin/ls","ls","-l","-a","--color",NULL,environ);
printf("process end...\n");
return 0;
}
④execv
V:类似于数组,这个函数就是将执行程序的选项都放入一个数组中,以NULL结尾,传数组
int execv(const char *path, char *const argv[]);
示例:
#include <stdio.h>
#include <unistd.h>
int main()
{
char* const arg[]={
(char*)"ls",
(char*)"-l",
(char*)"-a",
(char*)"--color",
NULL
};
printf("process begin....\n");
execv("/usr/bin/ls",arg);
printf("process end...\n");
return 0;
}
⑤execvp
int execvp(const char *file, char *const argv[]);
可以不带路径
#include <stdio.h>
#include <unistd.h>
int main()
{
char* const arg[]={
(char*)"ls",
(char*)"-l",
(char*)"-a",
(char*)"--color",
NULL
};
printf("process begin....\n");
execvp("ls",arg);
printf("process end...\n");
return 0;
}
⑥execvpe
int execvpe(const char *file, char *const argv[],
char *const envp[]);
以下示例为用C语言替换一个C++程序
C++程序
#include <iostream>
#include <unistd.h>
using namespace std;
int main(int argc,char* argv[],char* env[])
{
int i=0;
for(;argv[i];i++)
{
printf("argv[%d]: %s\n",i,argv[i]);
}
printf("-------------------------------------\n");
for(i=0;env[i];i++)
{
printf("env[%d]: %s\n",i,env[i]);
}
printf("-------------------------------------\n");
cout<<"hello i am c++ pragme!"<<endl;
cout<<"hello i am c++ pragme!"<<endl;
cout<<"hello i am c++ pragme!"<<endl;
cout<<"hello i am c++ pragme!"<<endl;
return 0;
}
C程序
#include <stdio.h>
#include <unistd.h>
int main()
{
char* const arg[]={
(char*)"myprame",
NULL
};
//自定义环境变量
char* const envp[]={
(char*)"HAHA=111111",
(char*)"HEHE=222222",
NULL
};
printf("process begin....\n");
execvpe("./cpppragme",arg,envp);//替换成自己写的C++程序
printf("process end...\n");
return 0;
}
可以看到替换后的C++程序使用的参数以及环境变量是从C语言程序传入的。这也再一次说明父进程为子进程创建两张表,一张为环境变量表,一张为参数表,最终都会想办法传给子进程!!