父子进程的关系
子进程作为父进程的副本,意味着子进程在创建时从父进程处继承了部分资源。具体来说,子进程获得了父进程的数据段、堆、栈和正文段的共享。这种共享使得子进程可以在一定程度上利用父进程已有的资源进行运行,但同时也需要注意资源的正确使用和管理,以避免出现冲突和错误。
1、父子进程运行顺序的不确定性
在 fork 之后,通常情况下无法确定父子进程哪个会先运行。这是因为操作系统的调度机制是动态的,它会根据系统的负载、各个进程的优先级以及其他因素来决定哪个进程先获得 CPU 时间片进行运行。如果非要确定父子进程的运行顺序,就需要借助 IPC(进程间通信)机制。通过特定的通信方式,可以让一个进程等待另一个进程完成某些操作后再继续执行,从而实现对运行顺序的控制。
2、父子进程的区别
1. fork 的返回值:父进程和子进程在 fork 函数的返回值上有明显的区别。在父进程中,fork 返回子进程的进程 ID;而在子进程中,fork 返回 0。这个返回值可以被用来区分当前进程是父进程还是子进程,从而执行不同的代码逻辑。
2. pid 不同:父子进程的进程 ID(pid)是不同的,且至少相差 1。每个进程在系统中都有唯一的 pid,通过 pid 可以区分不同的进程。子进程的 pid 是由操作系统分配的新值,与父进程的 pid 不同。
进程的终止
一、正常终止的情况
1. main中return:当主函数main执行到return语句时,进程会正常终止。这种方式比较直接,通常用于程序正常完成任务后的退出。
2. exit():这是 C 库函数,在进程终止时会执行 I/O 库的清理工作,关闭所有的流以及所有打开的文件。同时,它还会执行通过atexit注册的清理函数,确保在进程结束前完成一些必要的清理操作。
3. _exit和_Exit:这两个函数会直接关闭所有已经打开的文件,但不会执行清理函数。它们通常用于需要快速终止进程且不需要进行复杂清理工作的情况。
4. 主线程退出:当进程的主线程执行完毕或主动退出时,进程可能会终止。这取决于操作系统的调度和进程的具体实现。如果主线程是进程中唯一的执行线程,那么主线程的退出通常会导致进程的终止。
5. 主线程调用pthread_exit:如果主线程调用了线程退出函数pthread_exit,那么主线程会退出。如果进程中还有其他线程在运行,进程可能不会立即终止,具体情况取决于操作系统的实现和线程的属性。
二、异常终止的情况
6. abort():调用这个函数会导致进程异常终止,类似于发生了段错误。它通常用于在程序出现严重错误时强制终止进程,以便进行错误分析和调试。
7. signal和kill pid:通过发送信号可以导致进程异常终止。例如,可以使用kill命令向特定的进程发送信号,如SIGKILL信号可以强制终止进程。信号处理机制可以让进程在接收到特定信号时采取相应的行动,包括终止进程。
8. 最后一个线程被pthread_cancel:如果进程中的最后一个线程被取消(通过pthread_cancel函数),那么进程可能会终止。线程的取消可能是由于其他线程的请求或者由于某些特定的条件触发。
进程的退出
一、退出后的两种情况
进程退出后可能出现僵尸进程和孤儿进程两种情况。
僵尸进程是指进程执行结束但空间未被回收的状态。这种情况下,虽然进程已经完成了任务,但它在操作系统中的资源(如进程控制块等)没有被完全释放,会占用一定的系统资源。
孤儿进程则是指父进程先于子进程结束,使得子进程成为没有父进程的进程。在这种情况下,孤儿进程通常会被操作系统的特殊进程(如 init 进程)收养,以确保它们能够正常结束和资源回收。
二、进程退出函数
1. exit函数:
◦ 这是一个库函数,用于让进程退出。在退出时,它会通知父进程进程是如何终止的。如果是正常结束,由exit传入的参数表示退出状态;如果是异常终止,则由内核通知异常终止原因的状态。父进程可以使用wait或waitpid函数来获取这个状态以及进行资源回收。
◦ void exit(int status),其中status是进程退出的状态。例如exit(1)表示以特定状态退出。EXIT_SUCCESS通常定义为 0,表示成功退出;EXIT_FAILURE通常定义为 1,表示失败退出。
◦ 执行过程为:先刷新缓存区,然后依次调用通过atexit注册的退出函数,最后执行_exit系统调用。
2. _exit函数:
◦ 这是一个系统调用,功能也是让进程退出,但与exit不同的是,它不刷新缓存区。
◦ void _exit(int status),参数status表示进程退出状态。
3. atexit函数:
◦ 这是一个用于注册进程清理函数的函数,即当进程退出时执行的函数。调用越早注册的清理函数会在进程退出时越晚执行,按照注册顺序的倒序被调用。
◦ int atexit(void (*function)(void)),参数是一个函数指针,指向一个没有参数且返回值为void的函数。成功注册返回 0,失败返回非 0。
在实际编程中,合理使用这些函数可以确保进程在退出时进行必要的清理工作,避免资源泄漏和数据丢失。同时,对于僵尸进程和孤儿进程的处理也需要引起重视,以保证系统的稳定性和资源的有效利用。
#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;
}
进程空间的回收
一、进程空间回收的重要性
当一个进程结束后,它所占用的系统资源(如内存、文件描述符等)需要被回收,以避免资源泄漏和系统性能下降。特别是在多进程环境中,父进程通常需要负责回收子进程的资源,以确保系统的稳定运行。
二、使用wait和waitpid函数进行进程空间回收
1. wait函数:
◦ pid_t wait(int *status);这个函数可以阻塞等待任意子进程退出,并回收该进程的状态。一般用于父进程回收子进程的状态。
◦ 参数status用于接收子进程退出时的状态信息。如果不关心子进程的退出状态,可以用NULL表示。如果要回收子进程的退出状态,可以使用WEXITSTATUS等宏来获取具体的值。
◦ 返回值:成功时返回被回收的子进程的进程 ID(pid);失败时返回 -1。
2. 相关宏的作用:
1. 对于WIFEXITED(status)和WEXITSTATUS(status):
◦ WIFEXITED(status)用于判断子进程是否正常结束。如果子进程是通过调用exit函数或从main函数中返回正常退出的,那么这个宏会返回一个非零值。
◦ WEXITSTATUS(status)只有在WIFEXITED(status)返回非零值时才有意义,它用于提取子进程正常退出时的返回值。例如,如果子进程通过exit(42)退出,那么在父进程中可以使用WIFEXITED(status)判断子进程是否正常退出,然后使用WEXITSTATUS(status)获取子进程的退出状态码 42。
2. 对于WIFSIGNALED(status)和WTERMSIG(status):
◦ WIFSIGNALED(status)用于判断子进程是否是因为收到信号而终止的。如果子进程是被信号终止的,这个宏会返回一个非零值。
◦ WTERMSIG(status)在WIFSIGNALED(status)返回非零值时,可以用来获取导致子进程终止的信号编号。例如,如果子进程被信号SIGKILL终止,那么在父进程中可以使用WIFSIGNALED(status)判断子进程是否被信号终止,然后使用WTERMSIG(status)获取信号编号 9(SIGKILL的编号通常是 9)。
在这些宏中,status是一个整数参数,通常是由wait或waitpid函数返回的子进程的退出状态信息。
#include <stdio.h>
#include <unistd.h>>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
int main(int argc, const char *argv[])
{
pid_t pid = fork(); // 创建子进程
if (pid > 0)
{
printf("father pid = %d\n", getpid()); // 输出父进程的进程 ID
wait(NULL); // 父进程等待子进程结束,若不关心子进程退出状态,此处传入 NULL
printf("after wait\n"); // 子进程结束并被回收后输出
sleep(5); // 父进程睡眠 5 秒,模拟一些后续操作
}
else if (pid == 0)
{
printf("child pid = %d ppid = %d\n", getpid(), getppid()); // 输出子进程的进程 ID 和父进程 ID
sleep(3); // 子进程睡眠 3 秒,模拟一些操作
printf("child terminal\n"); // 子进程结束前输出
exit(0); // 子进程正常退出
}
else
{
perror("fork"); // fork 失败时输出错误信息
return 0;
}
return 0;
}
这段代码的用意是创建一个子进程,让子进程执行一些操作后退出,父进程等待子进程结束后继续执行一些操作。代码中wait让父进程等待子进程结束后再继续执行后续代码,保证程序的逻辑顺序。在这段代码中,父进程在子进程结束后才输出after wait,确保了父进程在子进程完成其任务后才进行下一步操作。
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
int main(int argc, const char *argv[])
{
// 使用 fork 函数创建一个子进程,返回子进程的进程 ID
pid_t pid = fork();
if (pid > 0)
{
// 如果返回值大于 0,表示处于父进程中
printf("father pid = %d\n", getpid());
// 定义一个整数变量 status,用于接收子进程的退出状态信息
int status;
// 调用 wait 函数等待子进程结束,并将子进程的退出状态存储在 status 中
pid_t ret = wait(&status);
// 如果子进程是正常结束的
if (WIFEXITED(status))
{
// 输出子进程正常结束的信息以及子进程的退出状态码
printf("子进程正常结束 %d\n", WEXITSTATUS(status));
}
// 如果子进程是被信号异常终止的
if (WIFSIGNALED(status))
{
// 输出子进程异常结束的信息以及导致子进程异常终止的信号编号
printf("子进程异常结束 %d\n", WTERMSIG(status));
}
// 输出等待子进程结束后的信息以及子进程的退出状态值
printf("after wait %d\n", status);
}
else if (pid == 0)
{
// 如果返回值等于 0,表示处于子进程中
printf("child pid = %d ppid = %d\n", getpid(), getppid());
// 子进程睡眠 5 秒,模拟子进程执行一些操作
sleep(5);
printf("child terminal\n");
// 子进程以状态码 50 正常退出
exit(50);
}
else
{
// 如果 fork 函数调用失败,返回值小于 0,输出错误信息
perror("fork");
return 0;
}
return 0;
}
正常结束和通过kill -9 来结束时的结果:
3.waitpid函数
pid_t waitpid(pid_t pid, int *status, int options);
1. pid参数:
◦ < -1:回收指定进程组内的任意子进程。
◦ -1:回收任意子进程,无论在组内还是组外。
◦ 0:回收和当前调用waitpid的进程一个组的所有子进程,即组内子进程。
◦ > 0:回收指定 ID 的子进程。
2. status参数:用于接收子进程退出时的状态信息。如果不关注子进程的退出状态,可以用NULL表示。
3. options参数:
◦ 0:表示回收过程会阻塞等待,直到有子进程退出。
◦ WNOHANG:表示非阻塞模式,即不会等待子进程退出就立即返回。在这种模式下,如果没有子进程退出,函数会立即返回 0。
二、与wait函数的关系
waitpid(-1, status, 0)等同于wait(status),它们都用于回收任意子进程的资源并获取子进程的退出状态。
三、返回值含义
1. 成功时:返回接收资源的子进程的进程 ID(pid)。
2. 失败时:返回 -1。如果指定了WNOHANG选项且没有子进程退出,也会返回 0。如果出现错误,可能会设置errno为ECHILD(没有子进程)或EINTR(被信号中断)等。
在实际编程中,可以根据具体需求选择使用wait或waitpid函数,以及合适的参数和选项来实现对子进程资源的有效回收和状态获取。非阻塞模式在需要同时处理多个任务而不想被等待子进程退出所阻塞时非常有用。
#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();
// 使用 fork 函数创建一个子进程,返回子进程的进程 ID
if (ret > 0)
{
//father
printf("father pid %d,ppid:%d \n", getpid(), getppid());
// 如果返回值大于 0,表示处于父进程中。输出父进程的进程 ID 和父进程的父进程 ID
int status;
// 定义一个整数变量 status,用于接收子进程的退出状态信息
while (1)
{
pid_t pid = waitpid(ret, &status, WNOHANG);
// 使用 waitpid 函数以非阻塞方式等待子进程结束,并将子进程的退出状态存储在 status 中
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");
// 如果 waitpid 返回值为 0,表示子进程未结束
}
}
printf("after wait, %d\n", status);
// 输出等待子进程结束后的信息以及子进程的退出状态值
}
else if (0 == ret)
{
//child
printf("child pid:%d ppid:%d\n", getpid(), getppid());
// 如果返回值等于 0,表示处于子进程中。输出子进程的进程 ID 和父进程 ID
sleep(5);
// 子进程睡眠 5 秒,模拟子进程执行一些操作
printf("child terminal\n");
// 子进程在即将结束前打印的消息,表示子进程完成了它的任务或到达了预定的结束点
exit(50);
// 子进程以状态码 50 正常退出
}
else
{
perror("fork error\n");
// 如果 fork 函数调用失败,返回值小于 0,输出错误信息
return 1;
}
return 0;
}
这段代码的主要功能是创建一个子进程,父进程以非阻塞方式等待子进程结束,并根据子进程的退出方式输出相应的信息,同时回收子进程的资源。如果子进程未结束,父进程会不断尝试等待,直到子进程结束或出现错误。
exec函数族
exec函数族用于在一个进程中启动另一个可执行文件,用新程序完全替换当前进程的用户空间代码和数据,从新程序的启动例程开始执行。执行exec函数后,进程的 ID 不会改变,因为它不是创建新进程,而是在现有进程的基础上替换其执行内容。
1.execl:
◦ int execl(const char *path, const char *arg,... /* (char *) NULL */);
◦ 使用路径名作为参数,第一个参数path是要执行的程序的绝对路径。后面的参数逐个列出新程序的参数,最后以(char *) NULL结束。例如execl("/bin/ls","-l","-a",NULL);,表示执行/bin/ls程序,参数为-l和-a。
2. execlp:
◦ int execlp(const char *file, const char *arg,... /* (char *) NULL */);
◦ 使用文件名作为参数,会在环境变量PATH指定的目录中查找可执行文件。参数的传递方式与execl相同。例如execlp("ls","-l","-a",NULL);,系统会在PATH指定的目录中查找名为ls的可执行文件并执行,参数为-l和-a。
3.execv:
◦ int execv(const char *path, char *const argv[]);
◦ 使用路径名作为参数,第二个参数是一个字符指针数组argv,数组的最后一个元素必须是NULL。这个数组包含了新程序的参数,与逐个列出参数的方式不同,这种方式更加方便处理参数较多的情况。例如char *args[] = {"/bin/ls","-l","-a",NULL}; execv("/bin/ls",args);。
4. execvp:
◦ int execvp(const char *file, char *const argv[]);
◦ 使用文件名作为参数,在环境变量PATH指定的目录中查找可执行文件。参数传递方式与execv相同。例如char *args[] = {"ls","-l","-a",NULL}; execvp("ls",args);。
#include <stdio.h>
#include <unistd.h>
int main(int argc, const char *argv[])
{
//会找当前目录下
//execl("bbb","bbb","1","2","3",NULL);
//只会在系统下找,需要写路径加文件名
//execlp("./bbb","bbb","1","2",NULL);
//同execl
//char *const arg[] = {"bbb","1","2","3","4",NULL};
//execv("bbb",arg);
//同execlp
char *const arg[] = {"bbb","1","2","3","4","5",NULL};
execvp("./bbb",arg);
//exec的所有的函数,第一个参数都可以写路径加文件名
printf("看见就出错\n");
return 0;
}