二、父子进程的关系:
1.父子进程的关系:
子进程是父进程的副本。子进程获得父进程数据段,堆,栈,正文段共享。
在fork之后,一般情况那个会先运行,是不确定的。如果非要确定那个要先运行,需要IPC机制。
区别:
1)fork的返回值
2)pid不同
继承和传递
-
资源继承:
-
子进程继承父进程的许多属性,如文件描述符、环境变量、工作目录等,但它们的进程 ID 是唯一的。
-
-
信号处理:
-
父进程可以设置信号处理程序来响应
SIGCHLD
信号,从而对子进程的状态变化作出反应。例如,当子进程退出时,父进程可以执行清理工作。
-
进程的终止:8中情况
1)main 中return
2)exit(), c库函数,会执行io库的清理工作,关闭所有 的流,以及所有打开的文件。已经清理函数(atexit)。
3)_exit,_Exit 会关闭所有的已经打开的文件,不执行清理函数。
4)主线程退出
5)主线程调用pthread_exit
异常终止
6)abort()
7)signal kill pid
8)最后一个线程被pthread_cancle
2.进程的退出
僵尸进程和孤儿进程
僵尸进程:进程执行结束但空间未被回收变成僵尸进程(PCB块默认不释放)
僵尸进程(Zombie Process)(会残留pcb块,大量占用系统内核)
定义:
-
僵尸进程是指一个已经终止执行但其进程表项仍然保留在系统中的进程。这是因为其父进程尚未读取该进程的退出状态。
产生原因:
-
当一个子进程终止时,它会发送一个
SIGCHLD
信号给其父进程,父进程通过调用wait()
或waitpid()
来读取子进程的退出状态。直到父进程这样做,子进程的进程表项将会被保留在系统中,状态为“僵尸”。 -
如果父进程没有调用
wait()
或waitpid()
来处理子进程的退出状态,那么子进程将会变成僵尸进程。
特征:
-
僵尸进程的进程表项存在,但它不占用系统资源,如 CPU 或内存。
-
僵尸进程的状态通常在
ps
命令中显示为Z
或z
(表示 Zombie)。
处理方式:
-
通常,父进程应当调用
wait()
或waitpid()
来收集子进程的退出状态,避免僵尸进程的产生。 -
如果父进程无法处理僵尸进程,系统会将其交给
init
进程(PID 1)处理,init
进程会调用wait()
以回收所有僵尸进程。
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
int main(int argc, char *argv[])
{
pid_t ret = fork();
if(ret>0)
{
//father
printf("father pid %d ,ppid:%d \n",getpid(),getppid());
sleep(10);
}
else if(0 == ret)
{
//child
printf("child pid:%d ppid:%d\n",getpid(),getppid());
exit(0);
}
else
{
perror("fork error\n");
return 1;
}
return 0;
}
孤儿进程(Orphan Process)
定义:(不影响)
-
孤儿进程是指其父进程已经终止,而它还在继续运行的进程。此时,这些孤儿进程会被系统的
init
进程(PID 1)收养。
产生原因:
-
当一个父进程在其子进程还在运行时终止,这些子进程变成孤儿进程。
init
进程会接管这些孤儿进程,并成为它们的新父进程。
特征:
-
孤儿进程的状态是正常的,只是它们的父进程已经结束。它们仍然在运行,可能继续执行它们的任务。
-
孤儿进程会被
init
进程收养,init
进程会负责回收这些进程的资源并处理其退出状态。
处理方式:
-
孤儿进程通常不需要特别处理,因为
init
进程会自动处理它们的生命周期。 -
不同于僵尸进程,孤儿进程不会无限期占用进程表项。
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
int main(int argc, char *argv[])
{
pid_t ret = fork();
if(ret>0)
{
//father
printf("father pid %d ,ppid:%d \n",getpid(),getppid());
exit(0);
}
else if(0 == ret)
{
//child
printf("child pid:%d ppid:%d\n",getpid(),getppid());
sleep(3);
printf("child pid:%d ppid:%d\n",getpid(),getppid());
}
else
{
perror("fork error\n");
return 1;
}
return 0;
}
1.exit 库函数
退出状态,终止的进程会通知父进程,自己使如何终止的。如果是正常结束(终止),则由exit传入的参数。如果是异常终止,则有内核通知异常终止原因的状态。任何情况下,负进程都能使用wait,waitpid获得这个状态,以及资源的回收。
void exit(int status)
exit(1);
功能:
让进程退出,并刷新缓存区
参数:
status:进程退出的状态
返回值:
缺省
EXIT_SUCCESS 0
EXIT_FAILURE 1
return 当该关键字出现在main函数中时候可以结束进程
如果在其他函数中则表示结束该函数。
exit -> 刷新缓存区 -> atexit注册的退出函数 -> _exit
-
参数
-
status
: 进程的退出状态码。通常情况下,返回值0
表示正常退出,而非零值表示异常或错误退出。具体的非零值可以用来表示不同的错误类型,具体取决于程序的约定或错误处理策略。
-
功能
-
清理操作:调用
exit
时,程序会进行一系列清理操作,包括关闭所有打开的文件描述符、清理分配的内存等。 -
执行终止处理函数:
exit
会调用所有通过atexit
注册的终止处理函数(clean-up functions)。 -
返回退出状态:
status
参数值会作为进程的退出状态返回给操作系统,父进程可以通过wait
或waitpid
来获取这个值。
-
行为
-
正常退出:如果
status
为0
,表示程序正常退出。 -
异常退出:如果
status
为非零值,表示程序异常退出或出现了错误。
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char *argv[])
{
FILE* fp = fopen("1.txt","w");
char buf[512]="hello,123";
fputs(buf,fp);
exit(0);
printf("aaaaaaaaaaa\n");
return 0;
}
2._exit 系统调用
void _exit(int status);
功能:
让进程退出,不刷新缓存区
参数:
status:进程退出状态
返回值:
缺省
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(int argc, char *argv[])
{
FILE* fp = fopen("1.txt","w");
char buf[512]="hello,123";
fputs(buf,fp);
_exit(0);// 不会刷新缓冲区
printf("aaaaaaaaaaa\n");
return 0;
}
3.atexit
int atexit(void (*function)(void));
功能:
注册进程退出前执行的函数
参数:
function:函数指针
指向void返回值void参数的函数指针
返回值:
成功返回0
失败返回非0
当程序调用exit或者由main函数执行return时,所有用atexit
注册的退出函数,将会由注册时顺序倒序被调用
退出前执行的函数为自己封装的函数
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
char * p ;
void clean(void)
{
printf("clean func,p %s\n",p);
free(p);
}
int main(int argc, char *argv[])
{
atexit(clean);
p = (char*)malloc(50);
strcpy(p,"hello");
printf("main p %s\n",p);
exit(0);
return 0;
}
3.进程空间的回收
exit(20);
wait/waitpid
pid_t wait(int *status);
功能:该函数可以阻塞等待任意子进程退出
并回收该进程的状态。
一般用于父进程回收子进程状态。
参数:status 进程退出时候的状态(包含了返回值)
如果不关心其退出状态一般用NULL表示
如果要回收进程退出状态,则用WEXITSTATUS回收。
返回值:成功 回收的子进程pid
失败 -1;
WIFEXITED(status) 是不是正常结束(宏返回真真代表子进程正常结束)
WEXITSTATUS(status) 使用这个宏去那返回值(子进程结束的返回值)
WIFSIGNALED(status) 是不是收到了信号而终止的
WTERMSIG(status)如果是信号终止的,那么是几号信号。
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h>
#include <stdlib.h>
int a = 20;
int main(int argc, char *argv[])
{
pid_t ret = fork();
if(ret>0)
{
//father
printf("father is %d pid %d ,ppid:%d \n",a,getpid(),getppid());
int status;
pid_t pid = wait(&status);
if(WIFEXITED(status))// 代表子进程正常结束
{
//正常结束的子进程,才能获得退出值
printf("child quit values %d\n",WEXITSTATUS(status));
}
if(WIFSIGNALED(status))//异常结束
{
printf("child unnormal signal num %d\n", WTERMSIG(status));
}
printf("after wait, %d\n",status);
}
else if(0 == ret)
{
//child
printf("child a is %d pid:%d ppid:%d\n",a,getpid(),getppid());
sleep(5);
printf("child terminal\n");
exit(50);
}
else
{
perror("fork error\n");
return 1;
}
printf("a is %d pid:%d\n",a,getpid());
return 0;
}
练习:
设计多进程测试程序,完成字符和字符串作为
exit返回的状态,查看是否能完成资源与数据
的回收。
pid_t wait(int *status);
1)如果所有的子进程都在运行,在阻塞
2)如果一个子进程终止,正在等待的父进程则获得终止状态,获得子进程的状态后,立刻返回。
3)如果没有子进程,则立即出错退出。
status
参数的处理
如果 status
不为 NULL
,在调用 waitpid
之后,可以使用以下宏来解析退出状态:
-
WIFEXITED(status)
: 如果子进程正常退出,则返回非零值。 -
WEXITSTATUS(status)
: 获取子进程的退出状态码(如果WIFEXITED
返回真)。 -
WIFSIGNALED(status)
: 如果子进程是因为信号终止的,则返回非零值。 -
WTERMSIG(status)
: 获取导致子进程终止的信号编号(如果WIFSIGNALED
返回真)。 -
WIFSTOPPED(status)
: 如果子进程因为信号而暂停,则返回非零值。 -
WSTOPSIG(status)
: 获取导致子进程暂停的信号编号(如果WIFSTOPPED
返回真)。 -
WIFCONTINUED(status)
: 如果子进程从暂停状态恢复,则返回非零值。
pid_t waitpid(pid_t pid, int *status, int options);
waitpid(-1,status,0)=wait(status);(两种写法等效)
参数
-
pid
: 要等待的子进程的进程 ID(PID)。-
pid > 0
:等待指定的子进程(PID 为pid
的子进程)。 -
pid == 0
:等待与当前进程属于同一个进程组的任意子进程。 -
pid < -1
:等待进程组 ID 为-pid
的所有子进程。 -
pid == -1
:等待所有子进程(类似于wait
函数的行为)。
-
-
status
: 指向一个整数的指针,用于存储子进程的退出状态。如果这个参数为NULL
,则不关心子进程的状态信息。 -
options
: 用于修改waitpid
的行为的标志。常用的选项包括:-
WNOHANG
:如果没有子进程退出,则立即返回而不阻塞。 -
WUNTRACED
:如果子进程因为收到信号而停止(暂停),也会返回状态信息。 -
WCONTINUED
:如果子进程因为收到信号而继续运行,也会返回状态信息。
-
返回值
-
成功:返回已退出的子进程的进程 ID(PID)。
-
失败:返回
-1
,并设置errno
以指示错误原因。
< -1 回收指定进程组内的任意子进程
-1 回收任意子进程,组内外
0 回收和当前调用waitpid一个组的所有子进程,组内
> 0 回收指定ID的子进程
waitpid (-1,a,0) == wait(a);
status 子进程退出时候的状态,
如果不关注退出状态用NULL;
options 选项:
0 表示回收过程会阻塞等待
WNOHANG 表示非阻塞模式回收资源。
返回值:成功 返回接收资源的子进程pid
失败 -1
0,
EAGAIN
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h>
#include <stdlib.h>
int main(int argc, char *argv[])
{
pid_t ret = fork();
if(ret>0)
{
//father
printf("father pid %d ,ppid:%d \n",getpid(),getppid());
int status;
while(1)
{
pid_t pid = waitpid(ret,&status, WNOHANG);
if(ret == pid)
{
if(WIFEXITED(status))// 代表子进程正常结束
{
//正常结束的子进程,才能获得退出值
printf("child quit values %d\n",WEXITSTATUS(status));
}
if(WIFSIGNALED(status))//异常结束
{
printf("child unnormal signal num %d\n", WTERMSIG(status));
}
break;
}
else if(0 == pid)
{
printf("子进程未结束,稍后在试\n");
}
}
printf("after wait, %d\n",status);
}
else if(0 == ret)
{
//child
printf("child pid:%d ppid:%d\n",getpid(),getppid());
sleep(5);
printf("child terminal\n");
exit(50);
}
else
{
perror("fork error\n");
return 1;
}
return 0;
}
练习:
设计一个多进程程序,用waitpid函数指定回收其中的某个进程资源并将其状态打印输出。其他的进程都以非阻塞方式进行资源回收
1
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h>
#include <stdlib.h>
int main(int argc, char *argv[])
{
int i = 0 ;
pid_t ret[5]={0};
printf("father pid %d ,ppid:%d \n",getpid(),getppid());
for(i = 0 ;i<5;i++)
{
ret[i] = fork();
if(ret[i]>0)
{
//father
}
else if(0 == ret[i])
{
//child
printf("child pid:%d ppid:%d\n",getpid(),getppid());
sleep(rand()%5);
exit(1);
}
else
{
perror("fork error\n");
return 1;
}
}
int status;
while(1)
{
pid_t pid = waitpid(ret[2],&status, WNOHANG);
if(ret[2] == pid)
{
if(WIFEXITED(status))// 代表子进程正常结束
{
//正常结束的子进程,才能获得退出值
printf("child quit values %d\n",WEXITSTATUS(status));
}
if(WIFSIGNALED(status))//异常结束
{
printf("child unnormal signal num %d\n", WTERMSIG(status));
}
printf("father recycle success, pid :%d\n",pid);
break;
}
else if(0 == pid)
{
printf("子进程未结束,稍后在试\n");
//usleep(1000);
sleep(1);
}
}
printf("after wait, %d\n",status);
return 0;
}
2
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h>
#include <stdlib.h>
int main(int argc, const char *argv[])
{
int ret[5] = {0};
printf("father pid:%d ppid:%d\n",getpid(),getppid());
int i;
for(i = 0;i < 5;++i)
{
ret[i] = fork();
if(ret[i] > 0)
{
continue;
}
else if(ret[i] == 0)
{
printf("child pid:%d ppid:%d\n",getpid(),getppid());
break;
}
else
{
perror("fork error!\n");
}
}
int status;
if(ret[4] > 0)
{
pid_t pid = waitpid(ret[0],&status,0);
if(pid == ret[0])
{
if(WIFEXITED(status))// 代表子进程正常结束
{
//正常结束的子进程,才能获得退出值
printf("child quit values %d\n",WEXITSTATUS(status));
}
if(WIFSIGNALED(status))//异常结束
{
printf("child unnormal signal num %d\n", WTERMSIG(status));
}
}
}
i = 0;
int j = 1;
if(ret[4] > 0)
{
while(1)
{
pid_t pid = waitpid(ret[j],&status,WNOHANG);
if(pid == ret[j])
{
++j;
++i;
if(i == 3)
{
break;
}
}
}
}
return 2;
}
exec
execute
exec族
用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。
其实有六种以exec开头的函数,统称exec函数:
vector
ls -l -i list
execl("/bin/ls","-l","-i",NULL);
execlp("ls","-l","-i",NULL);
int execl(const char *path, const char *arg, ...);
int execv(const char *path, char *const argv[]);
echo $PATH
PATH=
int execlp(const char *file, const char *arg, ...);
int execvp(const char *file, char *const argv[]);
这些函数的区别
1),前4个使用路径名作为参数,后面两个使用文件名做参数
当filename中,含有/时视为路径名,否则就按PATH变量,在指定目录下查找可执行文件。
2)相关的参数表传递
l表示list,v表示vector
execl,execlp,execle,需要将参数一个一个列出,并以NULL结尾。
execv,execvp,execve,需要构造一个参数指针数组,然后将数组的地址传入。
3)以e结尾的函数,可以传入一个指向环境字符串的指针数组的指针。其他未指定环境变量,使用父进程继承过来的。
execve 是真正的系统调用
这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回,如果调用出错
则返回-1,所以exec函数只有出错的返回值而没有成功的返回值。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(int argc, char *argv[])
{
// firefox www.baidu.com
execl("/usr/bin/firefox","firefox","www.baidu.com",NULL);
printf("看见就错了\n");
exit(1);
return 0;
}
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(int argc, char *argv[])
{
// firefox www.baidu.com
//execl("/usr/bin/firefox","firefox","www.baidu.com",NULL);
// env echo $PATH ls -l --color=auto ll
execlp("ls","ls","-l","--color=auto",NULL);
printf("看见就错了\n");
exit(1);
return 0;
}
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(int argc, char *argv[])
{
// firefox www.baidu.com
//execl("/usr/bin/firefox","firefox","www.baidu.com",NULL);
// env echo $PATH ls -l --color=auto ll
//execlp("ls","ls","-l","--color=auto",NULL);
char *const args[]={"ls","-l","--color=auto",NULL};
execv("/bin/ls",args);//vector
printf("看见就错了\n");
exit(1);
return 0;
}
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(int argc, char *argv[])
{
// firefox www.baidu.com
//execl("/usr/bin/firefox","firefox","www.baidu.com",NULL);
// env echo $PATH ls -l --color=auto ll
//execlp("ls","ls","-l","--color=auto",NULL);
char *const args[]={"ls","-l","--color=auto",NULL};
//execv("/bin/ls",args);//vector
// path
execvp(args[0],args);//vector+path
printf("看见就错了\n");
exit(1);
return 0;
}
带p的去path下寻找可执行的文件,只在path下找,不会检测自己当前的目录,添加./则会在当前路径下寻找(echo $PATH 显示带p的会检测的目录)(env,全部信息)
不带p的会优先扫描当前路径有没有可执行文件,正常需要传递具体路径
总结:全传路径加文件名,一定正确
eg:如果当前目录下有可执行文件aaa,如何调用
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(int argc, char *argv[])
{
// firefox www.baidu.com
//execl("./aaa","aaa","1","2","3",NULL);
// env echo $PATH ls -l --color=auto ll
//execlp("./aaa","aaa","1","2","3",NULL);
char *const args[]={"aaa","1","2","3",NULL};
//execv("./aaa",args);//vector
// path
execvp("./aaa",args);//vector+patha
//如果需要都能调用成功,第一个参数都传 路径+文件名
printf("看见就错了\n");
exit(1);
return 0;
}