进程控制
1. 进程创建
1.1 fork函数
1. 在linux中fork函数时非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。
#include <unistd.h>
pid_t fork(void);
返回值:自进程中返回0,父进程返回子进程id,出错返回-1

当一个进程调用fork之后,就有两个二进制代码相同的进程。而且它们都运行到相同的地方。但每个进程都将可以开始它们自己的旅程。
2.fork返回值
子进程返回0,创建失败返回-1;
父进程返回的是子进程的pid,为了让父进程方便对子进程进行标识,进而进行管理。
2. 进程的退出
2.1 引言
我们写代码时,main函数结束时的return 0,是干什么的?
2.2 进程退出码
进程退出时,会return一个值给父进程,这个值就是该进程的退出码。退出码的目的是告诉父进程,我的进程运行结果。而在Linux操作系统中,进程结束会将退出码传给bash,bash会将进程运行结果告诉用户。
2.3 查看退出码
echo $?\\echo是内建命令,查看bash内部环境变量的内容;?是bash获取的最近一个子进程退出码。
2.4 操作系统提供的退出码
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <string.h>
int main ()
{
for(int errcode=0;errcode<=225;errcode++)
{
printf("%d:%s\n",errcode,strerror(errcode));
}
return 0;
}
strerror:

该函数可以将错误码,转化成错误含义。

退出码,有他们的退出解释。
当退出码等于0,说明进程成功运行结束。当退出码不等于0,说明进程退出错误。
2.5 自定义环境退出码
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <string.h>
enum
{
Success=0,
Div_zero,
Mod_zero,
};//用枚举的方式定义退出码。
int exit_code=Success;
int Div(int a,int b)
{
if(b==0)
{
exit_code=Div_zero;
return -1;
}
return a/b;
}
const char *exit_reason(int code)//模拟strerror函数,将退出错误打印出去。
{
switch(code)
{
case 0:
return "Success";
case 1:
return "Div_zero";
case 2:
return("Mod_zero";
}
}
int main ()
{
Div(10,0);
printf("%s",exit_reason(exit_code)); //进程结束,打印进程结束的退出解锁。
return exit_code;
}
运行结果:

2.6 进程的三种退出
- 代码跑完,结果正确,退出码等于0。
- 代码跑完,结果错误,退出码不等于0。
- 代码运行时,出现了异常,提前退出了。一但出现了异常,退出码就没有意义了。kill -11 pid 该指令给进程发信号,使进程判断出野指针,使进程退出。本质是因为进程收到os发给进程的退出信号。
2.7 判断进程退出原因
进程退出会有一个退出信号和退出码,退出信号不为0,则是代码运行异常;退出信号为0,则判断退出码。
2.8 进程如何终止
- return 数字:该退出进程方式只能在main函数中生效;函数中的return是返回函数值,不能结束进程。
- exit(数字):等同于return 数字,只不过该函数,可以在别的函数中调用,退出进程。
#include <unistd.h>
#include <stdlib.h>
void exit(int status);
- _exit(数字):作用和exit一样。
exit和_exit区别:

exit会在执行完清理函数,刷新缓存,关闭流后,调用_exit;而_exit不会执行刷新缓冲,关闭流等。
3.进程等待
3.1 进程等待必要性
- 之前讲过,子进程退出,父进程如果不管不顾,就可能造成‘僵尸进程’的问题,进而造成内存泄漏。
- 另外,进程一旦变成僵尸状态,那就刀枪不入,“杀人不眨眼”的kill -9 也无能为力,因为谁也没有办法杀死一个已经死去的进程。
- 最后,父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是不对,或者是否正常退出。
- 父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息
3.2 wait方法
- wait函数等待父进程中任意一个子进程退出,获取子进程退出信息,知道子进程退出原因,解决子进程的僵尸问题,回收资源。

成功返回被等待进程pid,失败返回-1。 - 运行wait观察现象
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <string.h>
#include <sys/wait.h>
#include <stdlib.h>
void childrun()
{
int cn=0;
for(cn=0;cn<5;cn++)
{
printf("I am child process, pid: %d, ppid:%d, cnt: %d\n", getpid(), getppid(), cn);
sleep(1);
}
}
int main ()
{
pid_t id=fork();
if(id==0)
{
childrun(); //运行子进程
printf("child quit ...\n");
exit(1);//子进程退出
}
sleep(8); //观察僵尸进程
wait(NULL);
sleep(3); //观察wait后,子进程信息
return 0;
}

看上面进程信息,我们发现子进程先运行,结束后成为僵尸进程,再wait后,僵尸进程消失,最后父进程运行结束。
3.3 waitpid方法
pid_ t waitpid(pid_t pid, int *status, int options);
返回值:
当正常返回的时候waitpid返回收集到的子进程的进程ID;
如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0;
如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;
参数:
pid:
Pid=-1,等待任一个子进程。与wait等效。 Pid>0,等待其进程ID与pid相等的子进程。
status:
status是一个输出型型参数,传一个整型地址,它会改变地址指向内存的内容,即传回子进程退出码和退出信号的信息。
options:
WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进
程的ID。
#include <stdio.h>
2 #include <sys/types.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <sys/wait.h>
6 #include <stdlib.h>
7
8 void childrun()
9 {
10 int cn=0;
11 for(cn=0;cn<5;cn++)
12 {
13 printf("I am child process, pid: %d, ppid:%d, cnt: %d\n", getpid(), getppid(), cn);
14 sleep(1);
15 }
16
17 }
18
19 int main ()
20 {
21 pid_t id=fork();
22 if(id==0)
23 {
24 childrun();
25 printf("child quit ...\n");
26 exit(1);
27 }
28 sleep(8);
29 int status=0;//给waitpid传入一个输出参数,观察wtatus的值。
30 pid_t rid=waitpid(id,&status,0);
31 if(rid>0)
32 {
33 printf("wait success,pid=%d,status=%d\n",rid,status);
34 }
34 else{
35 printf("wait fail\n");
36 }
37 sleep(3);
38 return 0;
39
40 }

进程过程和wait一样。

前面我们讲了status是子进程的退出码和退出信号,那么一个值是如何保存两个信息的呢?

- wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充。
- 如果传递NULL,表示不关心子进程的退出状态信息。
- 否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。
- status不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究status低16比特位):
status指向的是一个整型空间,有32个比特位,但是我们只用低16个比特位,0到6的7个比特位存储的是终止信号,而7的一个比特位存储的一个叫core dump标志,8到15的8个比特位存储的是退出信号。
3.4 WIFEXITED和WEXITSTATUS
WIFEXITED和WEXITSTATUS为c语言库里面定义的两个宏。
WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
3.5 status值怎么转换成退出码和终止信号
- 进程正常退出
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <string.h>
#include <sys/wait.h>
#include <stdlib.h>
void childrun()
{
int cn=0;
for(cn=0;cn<5;cn++)
{
printf("I am child process, pid: %d, ppid:%d, cnt: %d\n", getpid(), getppid(), cn);
sleep(1);
}
}
int main ()
{
pid_t id=fork();
if(id==0)
{
childrun();
printf("child quit ...\n");
exit(1);
}
sleep(8);
int status=0;
pid_t rid=waitpid(id,&status,0);
if(rid>0)
{
printf("wait success,pid=%d,status=%d,quite code=%d,quite signal=%d\n",rid,status,(status>>8)&0xff,status&0x7f);
}//这里处理得到两个退出信息
else{
printf("wait fail,rid=%d\n",rid);
}
sleep(3);
return 0;
}

2. 进程被终止
我们让子进程访问并修改空指针:
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <string.h>
#include <sys/wait.h>
#include <stdlib.h>
void childrun()
{
int cn=0;
for(cn=0;cn<5;cn++)
{
printf("I am child process, pid: %d, ppid:%d, cnt: %d\n", getpid(), getppid(), cn);
sleep(1);
}
}
int main ()
{
pid_t id=fork();
int *p=NULL;
if(id==0)
{
childrun();
*p=20//这里使用空指针;
printf("child quit ...\n");
exit(1);
}
sleep(8);
int status=0;
pid_t rid=waitpid(id,&status,0);
if(rid>0)
{
printf("wait success,pid=%d,status=%d,quite code=%d,quite signal=%d\n",rid,status,(status>>8)&0xff,status&0x7f);
}
else{
printf("wait fail,rid=%d\n",rid);
}
sleep(3);
return 0;
}

终止信号为11,此时退出码无意义。

终止信号为11)SIGSEGV为访问无效空间,的终止信号。
3.6 等待阻塞
- 如果子进程已经退出,调用wait/waitpid时,wait/waitpid会立即返回,并且释放资源,获得子进程退出信息。
- 如果在任意时刻调用wait/waitpid,子进程存在且正常运行,则进程阻塞。
- 如果不存在该子进程,则立即出错返回。
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <string.h>
#include <sys/wait.h>
#include <stdlib.h>
void childrun()
{
int cn=0;
for(cn=0;cn<5;cn++)
{
printf("I am child process, pid: %d, ppid:%d, cnt: %d\n", getpid(), getppid(), cn);
sleep(1);
}
}
int main ()
{
pid_t id=fork();
if(id==0)
{
childrun();
printf("child quit ...\n");
exit(1);
}
sleep(8);
int status=0;
pid_t rid=waitpid(id+1,&status,0); //这里我们接受一个错误的进程id
if(rid>0)
{
printf("wait success,pid=%d,status=%d\n",rid,status);
}
else{
printf("wait fail,rid=%d\n",rid);
}
sleep(3);
return 0;
}

发现wait返回了-1,说明等待失败

僵尸进程没有消失,资源没有被回收。
3.7 非阻塞等待
waitpid等待子进程时,返回值大于0,则等待成功;小于0,等待失败,可能是没有该子进程;等于0,有该子进程,但是该子进程还在运行,需要再次重复等待。
我们需要使waitipd的options为WNOHANG,使waitpid默认非阻塞等待。父进程不是一直阻塞等待,而是先去执行其他代码,过一会重复等
待子进程的过程,就叫非阻塞等待。
非阻塞等待实现:
非阻塞等待一般都是使用“非阻塞等待 + 循环 = 非阻塞轮询”的方法实现。
#include <sys/types.h>
#include <unistd.h>
#include <string.h>
#include <sys/wait.h>
#include <stdlib.h>
void childrun()
{
int cn=0;
for(cn=0;cn<5;cn++)
{
printf("I am child process, pid: %d, ppid:%d, cnt: %d\n", getpid(), getppid(), cn);
sleep(1);
}
}
int main ()
{
pid_t id=fork();//创建子进程
if(id==0)//子进程运行
{
childrun();
printf("child quit ...\n");
exit(123);
}
else//父进程运行
{
while(1)
{
int status=0;
pid_t rid=waitpid(id,&status,WNOHANG);//非阻塞等待
if(rid>0)//非阻塞等待结束
{
if(WIFEXITED(status))
{
printf("wait success,pid=%d,status=%d,quite code=%d\n",rid,status,WEXITSTATUS(status));
}
else
{
printf("exit unnormal!,WEXITSTATUS=%d\n",WEXITSTATUS(status));
}
break;
}
else if(rid==0)//继续等待
{
printf("wait child\n");//父进程可以在这里执行别的任务了。
}
else{//等待失败
printf("wait fail,rid=%d\n",rid);
break;
}
}
}
sleep(3);
return 0;
}

4. 进程程序替换
4.1 什么是进程替换

进程替换指的是将进程的数据和代码替换成,磁盘中要执行的代码和数据,并不创建新的进程。
4.2 用子进程替换
为了不影响父进程的代码,我们可以用fork创建子进程,替换程序。

子进程创建,还没有修改时,和父进程的虚拟地址相同,虚拟地址指向的,物理地址相同。但子进程程序替换时,为了不影响父进程,子进程页表指向的物理内存,会程序开一块新的地址,存放代码和数据。
4.3 进程替换函数
#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[]);
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
l(list) : 表示参数采用列表
v(vector) : 参数用数组
p(path) : 有p自动搜索环境变量PATH
e(env) : 表示自己维护环境变量
函数解释 :
这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回。
如果调用出错则返回-1。
所以exec函数只有出错的返回值而没有成功的返回值。
- execv和execl
int execl(const char *path, const char *arg, …):
execlp("/usr/bin/ls","ls","-a","-l",NULL);//第一个是执行文件的绝对路径,第二个是执行文件名,
和所带的指令,以NULL结尾。
int execv(const char *path, char *const argv[]):
char *const argv[]=
{
"ls",
"-l",
"-a",
NULL//数组以NULL结尾。
};
execv("/usr/bin/ls",argv);//第一个是执行文件的绝对路径,第二个是执行文件名,和所带的指令所
存放的数组。
- execvp和execlp
int execlp(const char *path, const char *arg, …):
execlp("ls","ls","-a","-l",NULL);//第一个是执行文件的文件名,第二个是执行文件名,
和所带的指令,以NULL结尾。
int execvp(const char *path, char *const argv[]):
char *const argv[]=
{
"ls",
"-l",
"-a",
NULL//数组以NULL结尾。
};
execv("ls",argv);//第一个是执行文件的文件名,第二个是执行文件名,和所带的指令所
存放的数组。
这两个函数可以不带绝对路径,只带文件名,但是必须依靠环境变量PATH中有的路径来查找。
- execvpe和execle
int execvpe(const char *file, char *const argv[],char *const envp[]):
我们要让子进程进程替换一个需要执行的cpp程序,可以先创建一个代码,用makefile自动编译:
.PHONY:all
all:process test//定义一个为伪目标。
test:test.cpp
@g++ -o $@ $^
process:myprocess.c
@gcc -o $@ $^
@echo "success..."
.PHONY:clean
clean:
@rm -f process
@echo "clean Process"
替换的程序test.cpp:
#include <iostream>
using namespace std;
int main(int argve,char *argv[],char *env[])//命令行参数个数,命令行参数,环境变量
{
int i=0;
for(i;i<argve;i++)
{
cout<<argv[i]<<endl;
}
for(i=0;env[i];i++)
cout<<env[i]<<endl;
return 0;
}
父进程myprocess.c:
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <string.h>
#include <sys/wait.h>
#include <stdlib.h>
int main()
{
pid_t id=fork();
char *const argv[]=
{
"ls",
"-l",
"-a",
NULL
};
if(id==0)
{
printf("child pid:%d \n",getpid());
char *const argv[]=
{
"test",
"-a",
"-b",
NULL
};
extern char ** environ;//声明系统中自带的环境变量,否则编译器可能找不到。
execvpe("./test",argv,environ); //这里我们文件名没在环境变量PATH中,所以要带上路径。
exit(123);
}
else
{
sleep(1);
int status=0;
pid_t rid=waitpid(id,&status,WNOHANG);
}
sleep(3);
return 0;
}
int execle(const char *path, const char *arg,…, char * const envp[]):
这个函数除第二个变量用法不同外,其他都一样。
4.4 三种环境变量的使用
- char ** environ:系统自带的环境变量表。
extern char ** environ;
- char * envp[]:自定义的环境变量表。
char * envp[]=
{
"HAHA=112233",
"HEHE=334455",
NULL//结尾
}
3.char ** environ + int putenv(char *string):在系统环境变量基础上更改环境变量。
#include <stdlib.h>
int putenv(char *string);
char ** environ
putenv("HAHA=112233");
purenv("HEHE=334455");
Linux进程控制详解

被折叠的 条评论
为什么被折叠?



