前一篇文章我们大致掌握了进程的相关概念,下面我们就需要来具体的控制进程了。
fork()函数创建子进程
- fork()函数的作用是在父进程中创建子进程。
- fork()函数体:pid_t fork(void)
- fork()函数的头文件为#include<sys/types.h>与#include<unistd.h>
- 返回值:成功对父进程返回子进程id,对子进程返回0;失败: 失败:返回-1
getpid()函数与getppid()函数
- getpid()与getppid()的头文件与fork()函数相同
- getpid()的作用是获取当前进程的id
- getppid()的作用是子进程获取父进程的id
下面我们就来展示一段代码
#include<stdio.h>
#include<stdlib.h>
#include<sys/types.h>
#include<unistd.h>
int main()
{
pid_t pid = fork();
if(pid < 0){
perror("fork error");
return -1;
}
if(pid > 0){
printf("This is father ID: %d\n", getpid());
}
if(pid == 0){
printf("this is child ID: %d\n", getpid());
}
return 0;
}
深入理解fork()函数
- fork()函数的内层机制是什么?我们都知道进程的地址空间从上到下大致分别为栈区,共享区,堆区,静态数据区,代码段。那么子进程怎样从父进程中分离出来,而不破坏父进程呢,现在的答案是明确的引用计数。子进程一开始被复制出来就继承了父进程的内核空间,但是子进程并不会开辟自己的空间。如果我们改变了子进程的栈区,或者其它区域,那么子进程就会独立出自己的栈区,或者其它区域。
- 在此之上我们还有必要了解一下vfork()函数,它也是创建子进程的一个函数,但是这个函数已经快被淘汰了,因为他的机制是阻塞等待,子进程与父进程同用一块进程地址空间,为了不破坏父进程的程序逻辑,父进程必须等待子进程完毕之后才能继续运行。
进程等待
- 什么是进程等待?进程等待是为了让父进程获取子进程的推出状态
- 为什么需要进程等待?进程等待是为了避免产生僵尸进程
那么如何进行进程等待
- wait()函数
- waitpid()函数
wait()函数
- wait()函数作用:等待任意一个子进程退出,若没有子进程退出,则一直阻塞等待
- wait()函数的头文件为#include<sys/wait.h>与#include<sys/types.h>
- 函数体:pid_t wait(int* status)
- 参数:输出型参数,获取子进程退出状态,不关心可以设置为NULL,我们后面详细说明这个参数
- 返回值:成功返回等待进程pid,失败返回-1
演示一段wait函数的等待
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<errno.h>
int main()
{
pid_t pid = fork();
if(pid < 0){
perror("fork error");
}
if(pid == 0){
printf("this is child id:%d\n", getpid());
}
if(pid > 0){
printf("this is father id:%d\n", getpid());
if(wait(NULL) < 0){
if(errno == ECHILD){
perror("have not child process!\n");
}
else{
perror("wait error");
}
}
}
while(1){
printf("%d\n", getpid());
sleep(1);
}
return 0;
}
这里父进程阻塞在wait()函数处,直到子进程退出父进程才会运行
waitpid()函数
- waitpid()函数作用:(可以设置为非阻塞式)可以等待指定子进程退出,也可以等待任意一个子进程退出,默认阻塞。
- 头文件:#include<sys/wait.h>与#include<sys/types.h>
- 参数与返回值:在代码段详解
- 阻塞:为了完成一个功能发起一个函数调用,如果没有完成这个功能则一直挂起等待功能完成才返回
- 非阻塞:为了完成一个功能发起一个函数调用,如果现在不具备完成的条件, 则立即返回而不等待
子进程进程状态获取
- 退出状态用4个字节来存储
- 在wait的参数中存储了子进程的退出原因以及退出码而参数中只用了低16位两个字节用于存储这些信息。在这低16位中:高8位存储的是退出码,进程运行完毕时才会有;低7位存储进程引起异常退出信号值,只有进程异常退出才会有(一般判断进程是否正常退出,正常退出则低7位为0)。第8位存储core dump(核心转储)标志。
- statu >> 8 获取退出码
- statu & 0x7f判断进程是否正常退出
- 调用WTERMSIG(wstatus)返回一个布尔量来判断子进程是否正常退出
- 调用WIFEXITED(wstatus)返回子进程退出异常信号,若正常退出则此值为0
下面演示一段waitpid()函数与获取子进程状态代码
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/wait.h>
#include<errno.h>
int main()
{
int pid = -1;
pid = fork();
if(pid < 0)
{
exit(-1);
}
else if(pid == 0)
{
sleep(20);
exit(12);
}
//pid_t waitpid(pid_t pid, int *wstatus, int options);
// 功能:默认等待子进程的退出
// pid:
// -1 等待任意子进程
// >0 等待进程id==pid的子进程
// wstatus:
// 用于获取子进程退出状态码
// options:选项参数
// WNOHANG:如果没有子进程退出,则立即返回报错,如果有则回收资源
//
int wstatus;
//waitpid函数第一个参数给-1,第三个参数默认0:默认阻塞等待任意一个子进程退出,获取状态吗,释放资源,效果等同于wait函数
while(waitpid(pid, &wstatus, WNOHANG) == 0)
{
printf("have no child exit!!!\n");
sleep(2);
}
if(WIFEXITED(wstatus))
{
//代表进程运行完毕退出
printf("child process exit normal\n");
}
else if( WTERMSIG(wstatus))
{
//代表异常信号导致进程退出
printf("cause child process exit signal:%d\n",WTERMSIG(wstatus));
}
if((wstatus & 0x7f) == 0)
{
printf("child process exit code:%d\n",wstatus >> 8);
}
else if((wstatus >> 8) == 0)
{
printf("cause child process exit signal:%d\n",wstatus & 0x7f);
}
while(1)
{
printf("hello world\n");
sleep(1);
}
return 0;
}
程序替换
- 创建一个子进程大多时候,并不希望子进程跟父进程做同样的事情,而是希望能够运行其他的程序代码,这个时候就用到了程序替换,程序替换只是替换了代码段,初始化了数据区域,因此程序替换并不会重新创建虚拟地址空间和页表,只是替换了其中的内容。替换后这个程序将从入口函数开始运行。
- 替换方法:exec函数族(头文件#include<unistd.h>)
int execl(const char *path, const char *arg, … (char *) NULL );
int execlp(const char *file, const char *arg, … (char *) NULL );
int execle(const char *path, const char *arg, …, (char *) NULL, 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[]); - execl与execv的区别:
1.参数如何赋予(参数平铺,指针数组)
execl与execlp的区别:
1.execl:需要告诉操作系统这个程序文件的全部路径
2.execlp:不需要告诉路径,只需要告诉文件名即可,操作系统会自动到PATH中的路径去找程序文件
execl与execle:
1.execl 继承于父进程的环境变量
2.execle 需要由我们用户来组织环境变量 - 环境变量:这里不做详解,有兴趣的同学可以自学,这个很重要的!
下面展示一下程序替换代码,不做演示
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>
int main(int argc, char* argv[])
{
int i = 0;
for(i = 0; argv[i] != NULL; i++)
{
printf("argv[%d]:%s\n",i,argv[i]);
}
//exec函数族作用是程序替换,如果替换成功,代表运行的代码段已经不是以前的代码段,而是新程序,因此原来代码段以后的代码都不会运行,除非出错
//execl与execv区别:参数如何赋予(平铺,指针数组)
//
//execl与execlp区别:
// execl需要告诉系统这个程序文件的全路径 /bin/ls
// execlp不需要告诉路径,只需要告诉文件名称即可,会自动到PATH中的路径下寻找 ls
//
//execl与execle区别:
// execl 继承于父进程的环境变量
// execle 自己由我们用户来组织环境变量
if(execl("/bin/ls","ls","-l", NULL) < 0)
{
perror("ls error");
}
execlp("ls","ls","-l", NULL);
//execlv需要我们自己组织环境变量,以一个指针数组来存储
char* env[] = {"MYENV=nihaoa",NULL};
execle("./test_exec","test_exec",NULL,env);
char* param[] = {"ls","ls","-l", NULL};
execv("ls",param);
return 0;
}
进程终止
- exit()函数:在进程任意位置都可以退出进程,在进程推出之前会刷新缓冲区,关闭文件,等一系列操作
- _exit()函数:强行推出进程,什么操作也没有,直接释放全部资源
- return:只有在main函数中执行才会退出进程,在main中return跟调用exit函数效果一样