当前位置: 首页 > news >正文

linux进程的复制和替换

Linux 进程的复制与替换

一、主函数参数

在 C 语言里,main 函数能够接收参数,其标准形式如下:

int main(int argc, char* argv[], char* envp[]);
  • argc:代表命令行参数的数量,为整数类型。
  • argv:是一个字符串数组,用来存放命令行参数的具体内容,argv[0] 一般是程序自身的名称。
  • envp:也是字符串数组,用于存储环境变量的信息。

二、缓冲区机制

printf 函数不会马上把数据输出到屏幕,而是先存于缓冲区。只有在满足以下三种情况时,才会输出到屏幕:

  1. 缓冲区满:当缓冲区的数据量达到上限,就会将数据输出。
  2. 强制刷新:可以使用 fflush(stdout) 函数来强制把缓冲区的数据输出。
  3. 程序结束:程序结束时,缓冲区的数据会自动输出。

三、fork 复制进程

fork 函数用于创建一个新的进程,此进程为调用 fork 的进程的子进程。fork 调用一次会返回两次:在父进程中返回子进程的进程 ID,在子进程中返回 0;若创建失败则返回 -1。
在这里插入图片描述
图片解释:左边的框图是父进程,右边的框图是子进程,两个进程的id号不同,并且不在同一块物理内存中。

#include <stdio.h>
#include <unistd.h>int main() {pid_t pid = fork();if (pid < 0) {perror("fork");return 1;} else if (pid == 0) {// 子进程printf("子进程: PID = %d, 父进程 PID = %d\n", getpid(), getppid());} else {// 父进程printf("父进程: PID = %d, 子进程 PID = %d\n", getpid(), pid);}return 0;
}

四、僵死进程与孤儿进程

僵死进程

出现原因

在 Linux 系统中,若子进程先结束而父进程还未结束,子进程就会成为僵死进程。子进程调用 exit() 退出后,其退出码会存于进程控制块(PCB)中。Linux 要求父进程必须获取子进程的退出码,在父进程获取之前,子进程的进程实体消失,但 PCB 会保留,此时子进程处于僵死状态。僵死进程不占用内存,仅消耗少量内核资源,不过过多的僵死进程会致使进程号耗尽。

解决方法

父进程可主动调用 wait()waitpid() 函数来获取子进程的退出码,从而释放子进程的 PCB。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>int main() {pid_t pid = fork();if (pid < 0) {perror("fork");return 1;} else if (pid == 0) {// 子进程printf("子进程即将退出\n");exit(0);} else {// 父进程int status;wait(&status);//阻塞if (WIFEXITED(status)) {//判断子进程是否正常退出printf("子进程正常退出,退出码: %d\n", WEXITSTATUS(status));//获取退出码}}return 0;
}

孤儿进程

若父进程先结束,子进程会被 init 进程(PID 为 1)接管,init 进程会定期调用 wait() 来清理子进程,避免出现大量僵死进程。

五、wait 函数

wait 函数用于等待子进程结束,并获取其退出状态。相关函数及宏定义如下:

#include <sys/wait.h>// 等待任意子进程结束,返回子进程的 PID,若出错则返回 -1
pid_t wait(int *status);// 判断子进程是否正常退出
int WIFEXITED(int status);// 获取正常退出的子进程的退出码
int WEXITSTATUS(int status);

六、fork 常见代码问题

有换行符的情况

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>int main() {for (int i = 0; i < 2; i++) {fork();printf("A\n");}exit(0);
}

在这里插入图片描述

初始时,主进程进入循环,i = 0。执行 fork() 后创建子进程 1,父进程和子进程 1 都从 fork() 返回,继续执行后续代码,各自打印一个 A。之后循环变量 i++ 变为 1,再次进入循环,父进程和子进程 1 又分别创建子进程 2 和子进程 3,这四个进程在第二次循环中都会打印一个 A,所以总共会打印 4 个 A

无换行符的情况

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>int main() {for (int i = 0; i < 2; i++) {fork();printf("A");}exit(0);
}

由于没有换行符,printf 的输出会存于缓冲区。fork 创建子进程时,缓冲区的内容会被复制到子进程中。因此,最终会打印 8 个 A

七、系统调用与文件操作

系统调用和库函数的关系

系统调用是用户程序访问内核的接口,只要需要访问内核的数据或硬件资源,库函数通常会调用系统调用。系统调用是访问内核的桥梁。
在这里插入图片描述

文件读写操作

open 函数
#include <fcntl.h>// 打开或创建文件
int open(const char *path, int oflags, mode_t mode);
  • path:要打开或创建的文件的路径名,可以是绝对路径或相对路径。
  • oflags:指定打开文件的方式和选项,可由多个常量按位或组合而成,如 O_RDONLY(只读)、O_WRONLY(只写)、O_RDWR(读写)、O_CREAT(如果文件不存在,则创建它。需要在mode参数中指定新文件的权限)O_EXCL(与O_CREAT一起使用,如果文件已存在,则open调用失败。这可以用于确保创建的文件是新的,避免意外覆盖现有文件)等。
  • mode:当使用 O_CREAT 标志创建新文件时,用于指定新文件的权限位。
  • 返回值:返回一个整数类型的文件描述符,若出错则返回 -1。
write 函数
#include <unistd.h>// 向文件描述符对应的文件写入数据
ssize_t write(int fildes, const void *buf, size_t nbyte);
  • fildes:文件描述符。
  • buf:要写入的数据的缓冲区。
  • nbyte:要写入的字节数。
  • 返回值:返回实际写入的字节数,若出错则返回 -1。
read 函数
#include <unistd.h>// 从文件描述符对应的文件读取数据
ssize_t read(int fildes, void *buf, size_t nbyte);
  • fildes:文件描述符。
  • buf:用于存储读取数据的缓冲区。
  • nbyte:要读取的字节数。
  • 返回值:返回实际读取的字节数,若到达文件末尾则返回 0,若出错则返回 -1。
示例代码

在这里插入图片描述
(进阶)拷贝文件:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>int main(int argc, char* argv[])
{if(argc != 3){printf("argc err\n");exit(1);}char *filename = argv[1];char *newfilename = argv[2];int fdr = open(filename,O_RDONLY);int fdw = open(newfilename,O_WDONLY|O_CREAT,0600);if(fdr==-1||fdw==-1){printf("open err\n");exit(1);}int num = 0;char buff[1024] = {0};while((num=read(fdr,buff,1024))>0){write(fdw,buff,num);}close(fdr);close(fdw);exit(0);
}

思考:fdr和fdw的值分别是多少?

当进程运行的时候,系统默认打开三个文件,分别是stdin、stdout、stderr,在内核中会有一个文件表存放打开的文件(结构体数组),从0下标开始,所以fdr和fdw的值应该为3、4。
在这里插入图片描述

文件操作在 fork 下的实践

先打开文件再 fork 创建子进程

父进程打开的文件,在 fork 之后子进程也能访问。父进程和子进程共享该文件,同时操作时会共享文件偏移量。

fork 创建子进程再各自打开文件

子进程和父进程各自打开文件,它们的操作互不影响。

系统调用与库函数的区别

系统调用是操作系统提供给用户程序的接口,用于访问内核的功能;库函数是对系统调用的封装,为用户提供更方便的编程接口。系统调用通常需要陷入内核态,开销较大;库函数在用户态执行,开销相对较小。

系统调用的执行过程

写时拷贝

写时拷贝是一种推迟或免除数据拷贝的技术。传统的 fork 会直接将所有资源复制给新创建的进程,效率较低。Linux 中的 fork 使用写时拷贝技术,内核在调用 fork 时并不复制整个进程地址空间,而是让父进程和子进程在某些数据上实现共享(只读),只有在需要写入时才进行复制。

八、进程替换

进程替换用于将当前程序替换为另一个程序。通常结合 forkexec 系列函数使用。

exec 系列函数

exec 系列函数包括 execlexeclpexecleexecvexecvp 等,它们本质上都是调用 execve 实现的。

#include <unistd.h>// 带参数列表的进程替换
int execl(const char *path, const char *arg, ...);// 带参数列表且在环境变量 PATH 中查找程序的进程替换
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[]);// 带参数数组且在环境变量 PATH 中查找程序的进程替换
int execvp(const char *file, char *const argv[]);// 系统调用的进程替换
int execve(const char *filename, char *const argv[], char *const envp[]);

代码示例

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>int main(int argc, char* argv[], char* envp[]) {printf("main pid:%d\n", getpid());// execl("/usr/bin/ps", "ps", "-f", (char*)0);  // 替换进程为 ps,参数为 ps、-f// 成功不返回,失败才返回// execlp("ps", "ps", "-f", (char*)0);  // 区别在于第一个参数不需要加路径// execle("/usr/bin/ps", "ps", "-f", (char*)0, envp);char* myargv[] = {"ps", "-f", 0};// execv("/usr/bin/ps", myargv);// execvp("ps", myargv);execve("/usr/bin/ps", myargv, envp);printf("execl err\n");exit(1);
}

九、实践:自定义 bash 解释器

bash是一个命令解释器,用户通过Bash输入命令,与操作系统交互

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/wait.h>
#include <pwd.h>char* get_cmd(char buff[], char* myargv[]) {if (buff == NULL || myargv == NULL) {return NULL;}int i = 0;char* s = strtok(buff, " ");while (s != NULL) {myargv[i++] = s;s = strtok(NULL, " ");}myargv[i] = NULL;return myargv[0];
}void print_info() {int user_id = getuid();char *s = "$";if (user_id == 0) {s = "#";}struct passwd *ptr = getpwuid(user_id);if (ptr == NULL) {printf("mybash>>");fflush(stdout);return;}char hostname[128] = {0};gethostname(hostname, 128);char curr_dir[128] = {0};getcwd(curr_dir, 128);printf("\033[1;32m%s@%s\033[0m:\033[1;34m%s\033[0m%s ", ptr->pw_name, hostname, curr_dir, s);fflush(stdout);
}int main() {while (1) {print_info();char buff[128] = {0};fgets(buff, 128, stdin);buff[strlen(buff) - 1] = 0;char* myargv[10] = {0};char* cmd = get_cmd(buff, myargv);if (cmd == NULL) {continue;}if (strcmp(cmd, "exit") == 0) {break;}if (strcmp(cmd, "cd") == 0) {if (myargv[1] != NULL) {if (chdir(myargv[1]) == -1) {printf("cd err:%s\n", myargv[1]);}}continue;}pid_t pid = fork();if (pid == -1) {continue;}if (pid == 0) {execvp(cmd, myargv);printf("exec err\n");exit(1);}wait(NULL);}exit(0);
}

代码解释

下面将逐行对这段代码进行分析:

头文件部分
  • #include <stdio.h>:包含标准输入输出库,这能让程序使用像printffgets这类标准输入输出函数。
  • #include <stdlib.h>:包含标准库,其中有exit函数,可用于终止程序。
  • #include <unistd.h>:包含 Unix 标准库,其中有getuidgethostnamegetcwdchdirforkexecvp等函数。
  • #include <string.h>:包含字符串处理库,像strtokstrcmpstrlen等函数就出自该库。
  • #include <sys/wait.h>:包含系统等待相关的库,其中有wait函数,用于等待子进程结束。
  • #include <pwd.h>:包含密码文件处理库,有getpwuid函数,可根据用户 ID 获取用户信息。
get_cmd函数
  • if(buff == NULL || myargv == NULL):检查传入的参数是否为NULL,若为NULL就返回NULL
  • char* s = strtok(buff," ");:运用strtok函数把输入的字符串buff按空格分割成多个子字符串。
  • while(s != NULL):持续分割字符串,直至分割完毕。
  • myargv[i++] = s;:把分割后的子字符串依次存于myargv数组中。
  • s = strtok(NULL," ");:继续分割剩余的字符串。
  • myargv[i] = NULL;:在myargv数组末尾添加NULL,这是execvp函数的要求。
  • return myargv[0];:返回myargv数组的第一个元素,也就是命令名。
print_info函数
  • int user_id = getuid();:获取当前用户的 ID。
  • char *s = "$";:初始化命令提示符符号为$
  • if(user_id == 0):若用户 ID 为 0(也就是 root 用户),则将命令提示符符号设为#
  • struct passwd * ptr = getpwuid(user_id);:依据用户 ID 获取用户信息。
  • if(ptr == NULL):若获取用户信息失败,就输出默认的命令提示符mybash>>
  • char hostname[128] = {0};gethostname(hostname,128);:获取当前主机名。
  • char curr_dir[128] = {0};getcwd(curr_dir,128);:获取当前工作目录。
  • printf("\033[1;32m%s@%s\033[0m:\033[1;34m%s\033[0m%s ",ptr->pw_name,hostname,curr_dir,s);:输出格式化的命令提示符,包含用户名、主机名、当前工作目录和命令提示符符号。
  • fflush(stdout);:刷新标准输出缓冲区,确保命令提示符及时显示。
main函数
  • while(1):进入一个无限循环,持续等待用户输入命令。
  • print_info();:输出命令提示符。
  • char buff[128] = {0};fgets(buff,128,stdin);:从标准输入读取一行命令。
  • buff[strlen(buff)-1] = 0;:去除fgets函数读取的换行符。
  • char* myargv[10] = {0};char* cmd = get_cmd(buff,myargv);:对输入的命令进行分割,获取命令名和参数。
  • if(cmd == NULL):若命令为空,就跳过本次循环。
  • if(strcmp(cmd,"exit") == 0):若命令为exit,则跳出循环,结束程序。
  • if(strcmp(cmd,"cd") == 0):若命令为cd,则使用chdir函数更改当前工作目录,若更改失败则输出错误信息。
  • pid_t pid = fork();:创建一个子进程。
  • if(pid == -1):若创建子进程失败,就跳过本次循环。
  • if(pid == 0):若当前是子进程,就使用execvp函数执行命令,若执行失败则输出错误信息并退出子进程。
  • wait(NULL);:父进程等待子进程结束。
  • exit(0);:程序正常结束。

总的来说,这段代码实现了一个简单的 shell 程序,它能显示命令提示符,读取用户输入的命令,处理exitcd命令,并且能创建子进程来执行其他命令。

strtok 函数

strtok 用于将字符串按指定的分隔符分割成多个子字符串(标记)。首次调用时传入待分割的字符串和分隔符,后续调用传入 NULL 和分隔符,函数会继续从上次的位置分割剩余字符串。不过,由于使用了静态指针,strtok 不是线程安全的。

printf 不同打印方式
  • 颜色和样式设置printf("\033[显示方式;前景色;背景色m + 打印内容 + 结尾部分:\033[0m")
  • 清屏printf("\033[2J")
  • 重置光标printf("\033[H")

相关文章:

  • Cherry Studio的MCP协议集成与应用实践:从本地工具到云端服务的智能交互
  • Spring AI:简化人工智能功能应用程序开发
  • 数字时代,如何为个人信息与隐私筑牢安全防线?
  • Linux系统安装方式+适合初学者的发行版本
  • Python项目源码63:病历管理系统1.0(tkinter+sqlite3+matplotlib)
  • 泰迪杯特等奖案例学习资料:基于边缘计算与多模态融合的温室传感器故障自诊断系统设计
  • BBR 之 ProbeRTT 新改
  • 基于随机森林的糖尿病预测模型研究应用(python)
  • 颠覆者DeepSeek:从技术解析到实战指南——开源大模型如何重塑AI生态
  • 企业级分布式 MCP 方案
  • 单片机-STM32部分:0、学习资料汇总
  • HTML5+JavaScript实现连连看游戏之二
  • QT6(32)4.5常用按钮组件:Button 例题的代码实现
  • Exa MCP Server - AI 搜索服务中间件
  • 计算机网络01-网站数据传输过程
  • 第37课 绘制原理图——放置离页连接符
  • 【计算机视觉】三维视觉:Open3D:现代三维数据处理的全栈解决方案
  • 第4篇:服务层抽象与复用逻辑
  • Java 中 Unicode 字符与字符串的转换:深入解析与实践
  • 精益数据分析(38/126):SaaS模式的流失率计算优化与定价策略案例
  • 云南省政协原党组成员、秘书长车志敏接受审查调查
  • 挑大梁!一季度北上广等7省份进出口占外贸总值四分之三
  • 陈逸飞《黄河颂》人物造型与借鉴影像意义
  • 4月一二线城市新房价格环比上涨,沪杭涨幅居百城前列
  • 马克思主义理论研究教学名师系列访谈|丁晓强:马克思主义学者要更关注社会现实的需要
  • 5月起,这些新规将施行