进程控制总结

1. 进程创建

在Linux系统中fork()函数是非常重要的函数,它用来在一个已经存在的进程中创建一个新的进程。新进程成为子进程,原进程称为父进程。

//
#include <unistd.h>
pid_t fork(void);
返回值:自进程中返回0,父进程返回子进程id,出错返回-1

在这里插入图片描述
当一个进程调用fork之后,就有两个二进制代码相同的进程。而且它们都运行到相同的地方。但每个进程都将可以执行它们各自己的代码。即fork之前父进程独立执行,fork之后,父子两个执行流分别执行。注意,fork之后,谁先执行完全由调度器决定。

fork() 调用后,fork() 给父进程的返回值是子进程的 PID(>0),继续执行 after 之后的代码。fork()给子进程的返回值是 0,也从同一个 after 处开始执行。虽然执行路径相同,但各自拥有独立的用户空间(各自的堆栈、数据段等)。

为了提高效率,父子进程在 fork() 后并不立即复制所有物理页,而是共享这些页,只在进程尝试写入时才真正分配新页并复制内容。因此,fork() 本质上是浅拷贝,直到写时才深拷贝

总之,fork() 就是内核为子进程拷贝父进程的执行上下文和大部分资源,然后父子进程各自独立地从 fork 调用点(after)继续执行

fork常规用法

  1. 一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如,父进程等待客户端请求,生成子进程来处理请求。
  2. 一个进程要执行一个不同的程序。例如子进程从fork返回后,调用exec函数来完成进程替换。

2. 进程终止

进程退出的场景:

  1. 代码运行完毕,结果正确。
  2. 代码运行完毕,结果不正确。
  3. 代码异常终止。

进程常见退出方法:

  1. main() 中 return表示进程终止(函数return表示函数结束)。
    return是一种更常见的退出进程方法。执行return n等同于执行exit(n),因为调用main的运行时函数会将main的返回值当做 exit的参数。
    在这里插入图片描述
  2. 在代码任意位置调用exit()函数表示进程终止。
    在这里插入图片描述
  3. 使用系统调用 _exit终止进程。
    在这里插入图片描述

exit和_exit的区别:
exit最后也会调用exit, 但在调用exit之前,还做了其他工作:执行用户通过 atexit或on_exit定义的清理函数。关闭所有打开的流,所有的缓存数据均被写入。调用_exit。
在这里插入图片描述
通过下面代码也可以看出:
在这里插入图片描述

3. 进程等待

什么是进程等待?
进程等待(process waiting)通常指父进程为了同步子进程的结束并回收其资源,主动挂起自己,直到子进程状态发生变化(最常见的是结束)。父进程调用 wait() 或waitpid() 这样的系统调用,自己进入阻塞(waiting)状态,直到某个子进程退出或收到信号而改变状态。期间,父进程不会继续执行,直到子进程终止或满足指定条件。

为什么要等待?
子进程一旦退出,会先变成僵尸进程(zombie),它的 PCB 和退出状态还挂在系统里,直到父进程调用 wait*() 才释放这些资源。父进程通过 wait,解决子进程的僵尸问题,回收系统资源,避免造成内存泄漏。其次,父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是不对,或者是否正常退出。

进程等待的方法

  1. wait方法
#include <sys/types.h>
#include <sys/wait.h>
int *status	输出参数,子进程的“状态”存放位置;如果只关心进程结束,不想获取状态,可传 NULL。
返回值	
成功:返回结束(或被信号终止)的子进程的 PID;
失败:返回 −1,并设置 errno。
pid_t wait(int *status);
  1. 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:
 WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
 WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
 options:
 WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID。
如果子进程已经退出,调用wait/waitpid时,wait/waitpid会立即返回,并且释放资源,获得子进程退出信息。
如果在任意时刻调用wait/waitpid,子进程存在且正常运行,则进程可能阻塞。
如果不存在该子进程,则立即出错返回。

获取子进程status
wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充。
如果传递NULL,表示不关心子进程的退出状态信息
否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程
status不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究status低16比特位)。
在这里插入图片描述

使用wait等待子进程,并通过位操作解析status,获取子进程的退出信息:

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

int main()
{
	pid_t id = fork();
	
	if (0 == id)
	{
		printf("I am a child process! pid: %d\n", getpid());
		sleep(10);
		_exit(123);
	}
	else if (id > 0)
	{
		int status = 0;
		pid_t rid = wait(&status);
		if (rid > 0 && (status & 0x7F) == 0)
		{
			printf("exit_code:%d\n", ((status >> 8) & 0xFF));
		}
		else if (rid > 0)
		{
			printf("exit_single:%d\n", ((status) & 0x7F));
		}
		else
		{
			perror("waitpid");
		}
	}
	else
	{
		perror("fork");
	}
	return 0;
}

子进程睡眠10秒后正常结束,退出码和代码中一致是123:
在这里插入图片描述
在另一个进程kill掉子进程:
在这里插入图片描述
上面通过位操作解析 status 的值,也可以通过如下宏来解析status 的值:

1. WIFEXITED(status)
作用:判断进程是否正常退出。
原理:检查 status 的低 8 位是否非零,且未被信号终止。
实现:
#define WIFEXITED(status)  (((status) & 0x7F) == 0)

2. WEXITSTATUS(status)
作用:提取进程的退出码。
原理:取 status 的 8-15 位(即右移 8 位后取低 8 位)。
实现:
#define WEXITSTATUS(status) (((status) >> 8) & 0xFF)

使用waitpid等待子进程,并通过宏解析status,获取子进程的退出信息:

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
	pid_t id = fork();
	
	if (0 == id)
	{
		printf("I am a child process! pid: %d\n", getpid());
		sleep(10);
		_exit(123);
	}
	else if (id > 0)
	{
		int status = 0;
		pid_t rid = waitpid(id, &status, 0);

		if (rid > 0 && WIFEXITED(status))
		{
			printf("exit_code:%d\n", WEXITSTATUS(status));
		}
		else 
		{
			printf("wait child process failed, return\n");
		}
	}
	else
	{
		perror("fork");
	}
	return 0;
}

进程的阻塞等待和非阻塞等待
1.阻塞等待
在 Linux 系统编程中,如果需要让 waitpid 阻塞等待子进程终止(即父进程暂停执行,直到目标子进程退出或被信号终止),应将 options 参数设置为 0(即不启用任何特殊选项)。此时 waitpid 的行为与 wait 类似。

调用 waitpid 后,父进程暂停执行,直到子进程状态变化(如终止、被信号杀死、暂停等)。父进程必须等待子进程完成后才能继续。

阻塞等待流程:

父进程代码
   │
   ▼
调用 wait()waitpid(..., 0)
   │
   ▼
父进程暂停执行,进入阻塞状态(等待子进程终止)
   │          ↙ 子进程运行中...
   ▼        ↙
子进程终止,内核通知父进程
   │
   ▼
父进程恢复执行,处理子进程状态
   │
   ▼
继续后续代码

示例代码:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

int main() 
{
    pid_t id = fork();
    if (id == 0) 
    {
        // 子进程执行任务
        sleep(2);
        exit(42); // 退出码 42
    } 
    else 
    {
        // 父进程阻塞等待指定子进程
        int status;
        pid_t rid = waitpid(id, &status, 0); // options=0,阻塞等待
        if (rid == id && WIFEXITED(status)) 
        {
            printf("child  %d 退出,状态码: %d\n", id, WEXITSTATUS(status)); // 输出 42
        } 
        else 
        {
            perror("waitpid 失败");
        }
    }
    return 0;
}

2.非阻塞等待
如果需要让 waitpid 非阻塞等待子进程终止,应将 options 参数设置为WNOHANG
调用 waitpid 后,父进程立即返回,可继续执行其他任务。需循环调用 waitpid 检查子进程状态(轮询机制)。

非阻塞等待流程:

父进程代码
   │
   ▼
调用 waitpid(..., WNOHANG)
   │
   ▼
内核检查子进程状态:
   ├─ 子进程已终止 → 立即返回 PID,父进程处理状态
   └─ 子进程未终止 → 返回 0,父进程继续执行其他任务
   │
   ▼
父进程循环调用 waitpid 轮询子进程状态
   │
   ▼
直到子进程终止,父进程处理状态
   │
   ▼
继续后续代码

示例代码:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

int main() 
{
    pid_t id = fork();
    if (id == 0) 
	{
        sleep(2);       // 子进程运行 2 秒
        exit(42);
    } 
	else 
	{
        int status;
        while (1) 
		{
            // 非阻塞等待子进程
            pid_t rid = waitpid(id, &status, WNOHANG);
            if (rid == id && WIFEXITED(status)) 
			{
                printf("子进程退出,状态码: %d\n", WEXITSTATUS(status));
                break;
            } 
			else if (rid == 0) 
			{
                printf("子进程未退出,父进程继续工作...\n");
                sleep(1);  // 模拟父进程执行其他任务
            } 
			else 
			{
                perror("waitpid 错误");
                break;
            }
        }
    }
    return 0;
}

3.形象比喻

阻塞等待: 你在一家咖啡店排队,必须等到咖啡做好后才能离开(父进程挂起,直到子进程完成)。
非阻塞等待:你点单后拿到一个取餐号,可以继续逛街,每隔一段时间回来问咖啡是否做好(父进程轮询检查子进程状态)。

4. 进程替换

4.1 exec 系列函数

在 Linux 系统编程中,进程替换通过 exec 系列函数实现,它的核心作用是将当前进程的代码段、数据段等替换为新的程序,使当前进程转而执行另一个程序
以下是 exec 系列函数的详细解析:

exec 系列函数的的核心作用
用新程序的代码、数据、堆栈等替换当前进程的原有内容。PID 保持不变(不创建新进程),原进程的打开文件描述符、信号处理等属性默认保留。exec 成功后,原进程的代码不再执行(被新程序完全替代)。

exec 系列函数的的命名规则和原型
所有 exec 函数均定义在 <unistd.h> 中,命名遵循以下规则:
后缀 l(list):参数以 可变参数列表 形式传递(NULL 结尾)。
后缀 v(vector):参数以 字符串数组 形式传递。
后缀 e(environment):允许自定义环境变量(需额外传递 envp 数组)。
后缀 p(PATH):自动在 PATH 环境变量指定的目录中搜索可执行文件。

函数原型特点
int execl(const char *path, const char *arg0, …, NULL)参数列表形式,需指定完整路径。
int execlp(const char *file, const char *arg0, …, NULL)参数列表形式,自动搜索 PATH。
int execle(const char *path, const char *arg0, …, NULL, char *const envp[])参数列表形式,可自定义环境变量。
int execv(const char *path, char *const argv[])参数数组形式,需指定完整路径。
int execvp(const char *file, char *const argv[])参数数组形式,自动搜索 PATH。
int execvpe(const char *file, char *const argv[], char *const envp[])参数数组形式,自动搜索 PATH 并自定义环境变量。

exec 系列函数使用示例

  1. execl 使用示例:
#include <stdio.h>
#include <unistd.h>

int main()
{
	printf("进程启动, 即将被替换为ls命令!!!\n");
	//path:可执行文件的 完整路径(如 /usr/bin/ls)。
	//arg0, ..., NULL:参数列表,以 NULL 结尾。
	//第一个参数 arg0 通常是程序名称(即 argv[0])。
	execl("/usr/bin/ls", "ls", "-a", "-l", NULL);

	perror("execl 失败");
	return 0;
}
  1. execlp 使用示例:
#include <stdio.h>
#include <unistd.h>

int main()
{
	printf("进程启动, 即将被替换为ls命令!!!\n");
	//file:可执行文件的 名称(如 ls),系统会在 PATH 中搜索
	//arg0, ..., NULL:参数列表,以 NULL 结尾。
	//第一个参数 arg0 通常是程序名称(即 argv[0])。
	execlp("ls", "ls", "-a", "-l", NULL);

	perror("execp 失败");
	return 0;
}
  1. execle 原使用示例:
#include <stdio.h>
#include <unistd.h>

int main()
{
	printf("进程启动, 即将被替换为ls命令!!!\n");
	//path:可执行文件的 完整路径(如 /usr/bin/ls)。
	//arg0, ..., NULL:参数列表,以 NULL 结尾。
	//第一个参数 arg0 通常是程序名称(即 argv[0])。
	//env:自定义环境变量数组,以 NULL 结尾。
	char* env[] = {"USER=ZhuZebo", NULL};
	execle("/usr/bin/ls", "ls", "-a", "-l", NULL, env);

	perror("execle 失败");
	return 0;
}
  1. execv 原使用示例:
#include <stdio.h>
#include <unistd.h>

int main()
{
	printf("进程启动, 即将被替换为ls命令!!!\n");
	//path:可执行文件的 完整路径(如 /usr/bin/ls)。
	//argv:参数数组,以 NULL 结尾。
	//argv[0] 通常是程序名称。
	char* argv[] = {"ls", "-a", "-l", NULL};
	execv("/usr/bin/ls", argv);
	
	perror("execv 失败\n");
	return 0;
}
  1. execvp 原使用示例:
#include <stdio.h>
#include <unistd.h>

int main()
{
	printf("进程启动, 即将被替换为ls命令!!!\n");
	//file:可执行文件的 名称(如 ls),系统会在 PATH 中搜索
	//argv:参数数组,以 NULL 结尾。
	//argv[0] 通常是程序名称。
	char* argv[] = {"ls", "-a", "-l", NULL};
	execvp("ls", argv);
	
	perror("execvp 失败\n");
	return 0;
}
  1. execvpe 原使用示例:
#include <stdio.h>
#include <unistd.h>

int main()
{
	printf("进程启动, 即将被替换为ls命令!!!\n");
	//file:可执行文件的 名称(如 ls),系统会在 PATH 中搜索
	//argv:参数数组,以 NULL 结尾。
	//argv[0] 通常是程序名称。
	//env:自定义环境变量数组,以 NULL 结尾。
	char* argv[] = {"ls", "-a", "-l", NULL};
	char* env[] = { "USER=ZhuZebo", NULL};
	execvpe("ls", argv, env);
	
	perror("execvpe 失败\n");
	return 0;
}

4.2 替换原理

程序替换通过 exec 系列函数实现,其本质是 将当前进程的代码和数据替换为新程序的代码和数据,但保持以下属性不变:
进程标识符(PID):进程的 ID 不变。
内核态信息:文件描述符表、进程优先级等。
在这里插入图片描述
替换步骤:
1.内核验证与权限检查:检查文件路径、权限、格式。
2.解析可执行文件(ELF):读取代码段、数据段、入口地址。
3 释放原进程的内存资源:释放原进程内存,加载新程序到虚拟地址空间。
4. 继承与重置属性:保留文件描述符,重置堆栈,恢复默认信号处理。
5.跳转到新程序入口:CPU 从新入口地址开始执行。
程序替换通过替换进程的代码和数据段实现进程重生

至此,本片文章就结束了,若本篇内容对您有所帮助,请三连点赞,关注,收藏支持下。
创作不易,白嫖不好,各位的支持和认可,就是我创作的最大动力,我们下篇文章见!
如果本篇博客有任何错误,请批评指教,不胜感激 !!!
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值