linux进程的复制和替换
Linux 进程的复制与替换
一、主函数参数
在 C 语言里,main
函数能够接收参数,其标准形式如下:
int main(int argc, char* argv[], char* envp[]);
argc
:代表命令行参数的数量,为整数类型。argv
:是一个字符串数组,用来存放命令行参数的具体内容,argv[0]
一般是程序自身的名称。envp
:也是字符串数组,用于存储环境变量的信息。
二、缓冲区机制
printf
函数不会马上把数据输出到屏幕,而是先存于缓冲区。只有在满足以下三种情况时,才会输出到屏幕:
- 缓冲区满:当缓冲区的数据量达到上限,就会将数据输出。
- 强制刷新:可以使用
fflush(stdout)
函数来强制把缓冲区的数据输出。 - 程序结束:程序结束时,缓冲区的数据会自动输出。
三、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
时并不复制整个进程地址空间,而是让父进程和子进程在某些数据上实现共享(只读),只有在需要写入时才进行复制。
八、进程替换
进程替换用于将当前程序替换为另一个程序。通常结合 fork
和 exec
系列函数使用。
exec
系列函数
exec
系列函数包括 execl
、execlp
、execle
、execv
、execvp
等,它们本质上都是调用 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>
:包含标准输入输出库,这能让程序使用像printf
、fgets
这类标准输入输出函数。#include <stdlib.h>
:包含标准库,其中有exit
函数,可用于终止程序。#include <unistd.h>
:包含 Unix 标准库,其中有getuid
、gethostname
、getcwd
、chdir
、fork
、execvp
等函数。#include <string.h>
:包含字符串处理库,像strtok
、strcmp
、strlen
等函数就出自该库。#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 程序,它能显示命令提示符,读取用户输入的命令,处理exit
和cd
命令,并且能创建子进程来执行其他命令。
strtok
函数
strtok
用于将字符串按指定的分隔符分割成多个子字符串(标记)。首次调用时传入待分割的字符串和分隔符,后续调用传入 NULL
和分隔符,函数会继续从上次的位置分割剩余字符串。不过,由于使用了静态指针,strtok
不是线程安全的。
printf
不同打印方式
- 颜色和样式设置:
printf("\033[显示方式;前景色;背景色m + 打印内容 + 结尾部分:\033[0m")
- 清屏:
printf("\033[2J")
- 重置光标:
printf("\033[H")