目录
1. 学习思维导图
2.进程创建(fork)
2.1 fork创建进程失败
3.进程终止
3.1 进程退出情况
3.1.1main函数
3.1.2 退出码
3.2 exit/_exit函数
1. exit() 函数
2. _exit() 函数
4.进程等待
4.1 实现进程等待的方法
wait/waitpid方法
区别:
4.2 status参数
5.进程替换
5.1 认识exec*系列函数
6.自定义Shell
1. 学习思维导图
2.进程创建(fork)
-
目标:理解进程创建的原理及
fork
函数的使用。
步骤:
创建成功会返回给父进程子进程的PID, 返回给子进程0,创建失败返回给父进-1。
实例代码:
- 阅读文档中
fork
函数的定义和返回值逻辑。 - 理解父子进程的执行流程及输出结果
-
实例代码:
#include <stdio.h>
#include <unistd.h>
int main()
{
pid_t id = fork();
if(id == 0)
{
// 子进程
printf("我是一个子进程,我的pid:%d,ppid:%d\n", getpid(), getppid());
}
else if(id >= 1)
{
// 父进程
printf("我是一个父进程,我的pid:%d\n", getpid());
}
return 0;
}
[ouyang@iZ2ze0j6dd76e0o9qypo2rZ dir_2025_2_9]$ ./process
我是一个父进程,我的pid:1297
我是一个子进程,我的pid:1298,ppid:1297
三个问题:
-
为什么fork()函数有多个返回值?
父进程创建子进程后,子进程的PCB指向的是父进程的代码和数据,由于进程是独立的,所以fork()函数被调用了两次(父和子),因此返回值有多个!
-
为什么返回给父进程子进程的pid,而返回子进程0
因为子进程归父进程管理,为了方便管理和找到子进程所以父进程会被返回子进程的pid。
子进程 : 父进程 ----> n : 1 ,一个父进程可以拥有多个子进程
-
为什么id可以接受多个返回值,并且使得if语句同时成立?
我们先打印父子进程的变量id地址观察:
我是一个父进程,我的pid:2198
id:0x7ffd0f3e500c
我是一个子进程,我的pid:2199,ppid:2198
id:0x7ffd0f3e500c
我们会发现地址是一样的,那么id的多值行为怎么解释呢? ---> 写时拷贝,而计算机给我们的id地址是一份虚拟地址。
2.1 fork创建进程失败
-
创建进程数超过最大数
-
空间资源不够创建进程
3.进程终止
3.1 进程退出情况
-
代码运行完毕,结果正确
-
代码运行完毕,结果不正确
-
代码异常终止
3.1.1main函数
-
main函数我们常常写成这样:
int main()
{
return 0;
}
-
main函数的返回值返回给谁了?
-
为什么通常返回0,有什么特殊含义吗?
main函数作为程序的入口函数,通常代表一个程序的结果,如果一个程序正常运行完毕,结果正确返回值为0,如果结果错误返回值为!0,如果代码程序异常终止那么返回值无意义!返回值通常存储在寄存器中进行返回!
-
返回结果正确返回值为0不用管。
-
返回值不正确非0,不同的值代表不同的问题!
案例:打开一个不存在的文件:
查看退出码(返回值):
命令: echo $? #打印最近一个程序的退出码
[ouyang@iZ2ze0j6dd76e0o9qypo2rZ dir_2025_2_11]$ ./process
[ouyang@iZ2ze0j6dd76e0o9qypo2rZ dir_2025_2_11]$ echo $?
1
# Bash进程会收集子进程process的退出码
# 这个退出码会写进进程的task_struct(PCB)
查看所有错误码的信息:
int main()
{
for(int i = 0; i < 150; ++i)
printf("strerror[%d]->%s\n", i, strerror[i]);
return 0;
}
[ouyang@iZ2ze0j6dd76e0o9qypo2rZ dir_2025_2_11]$ ./process
strerror[0]->Success
strerror[1]->Operation not permitted
strerror[2]->No such file or directory
strerror[3]->No such process
strerror[4]->Interrupted system call
strerror[5]->Input/output error
strerror[6]->No such device or address
strerror[7]->Argument list too long
strerror[8]->Exec format error
strerror[9]->Bad file descriptor
strerror[10]->No child processes
strerror[11]->Resource temporarily unavailable
strerror[12]->Cannot allocate memory
strerror[13]->Permission denied
strerror[14]->Bad address
strerror[15]->Block device required
strerror[16]->Device or resource busy
strerror[17]->File exists
strerror[18]->Invalid cross-device link
strerror[19]->No such device
strerror[20]->Not a directory
strerror[21]->Is a directory
........................................................
返回一个错误码,直接使用return errno;
[ouyang@iZ2ze0j6dd76e0o9qypo2rZ dir_2025_2_11]$ ./process
[ouyang@iZ2ze0j6dd76e0o9qypo2rZ dir_2025_2_11]$ echo $?
2
# strerror[1]->Operation not permitted
3.1.2 退出码
退出码(退出状态)可以告诉我们最后⼀次执⾏的命令的状态。在命令结束以后,我们可以知道命令是成功完成的还是以错误结束的。其基本思想是,程序返回退出代码 0 时表⽰执⾏成功,没有问题。代码 1 或 0 以外的任何代码都被视为不成功。
Linux Shell中的主要退出码:
3.2 exit/_exit函数
1. exit()
函数
-
定义:属于C标准库(
stdlib.h
),是高层级的进程终止函数。 -
行为:
-
执行清理操作:刷新所有标准I/O缓冲区(如
printf
的输出),关闭已打开的文件流。 -
调用通过
atexit()
注册的退出处理函数。 -
最后调用
_exit()
终止进程,向操作系统传递退出状态。
-
-
使用场景:通常用于正常终止程序,确保资源正确释放。
#include <stdlib.h>
int main() {
printf("Hello"); // 无换行,但exit会刷新缓冲区
exit(0); // 输出"Hello"
}
2. _exit()
函数
-
定义:属于系统调用(
unistd.h
),是低层级的进程终止函数。 -
行为:
-
直接终止进程,不刷新I/O缓冲区,不调用退出处理函数。
-
立即将控制权交还给操作系统。
-
-
使用场景:多用于子进程(如
fork()
后),避免干扰父进程的I/O状态。
示例:
#include <unistd.h>
int main() {
printf("Hello"); // 无换行
_exit(0); // 直接终止,不输出"Hello"
}
典型应用场景
-
子进程终止:在
fork()
后的子进程中,若直接调用exit()
可能导致父进程的I/O缓冲区被重复刷新(如输出两次)。此时应使用_exit()
:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main() {
if (fork() == 0) { // 子进程
printf("Child");
_exit(0); // 避免刷新父进程的缓冲区
} else { // 父进程
wait(NULL);
printf("Parent");
exit(0);
}
}
-
立即终止:当程序遇到致命错误且无需清理时(如
exec
失败后的子进程)。
总结
-
需要资源清理(如保存数据)时用
exit()
。 -
需要立即终止(如避免缓冲区冲突)时用
_exit()
。 -
库函数
exit()
是对_exit()
的封装! --> 通过这两个调用我们发现一件事情关于我们谈论的缓冲区的位置,我们知道一定不是系统缓冲区之中,我们使用的是库提供的缓冲区!
4.进程等待
前言:我们之前了解了僵尸进程,而进程等待就是为了解决僵尸进程的问题的!
进程等待的必要性:
-
子进程接受父进程的任务安排,是否完成任务都会有一个返回值,父进程接受这个返回值的过程可以称作为进程等待
-
进程等待是父进程的一种手段,是为了实现对子进程的资源回收(最主要的),和对子进程的进程退出信息的捕获(可以选择的,我们知道一个函数不是必须要有一个返回值,这边也是这种意思)。
4.1 实现进程等待的方法
wait/waitpid方法
WAIT(2) Linux Programmer's Manual WAIT(2)
NAME
wait, waitpid, waitid - wait for process to change state
SYNOPSIS
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status);
pid_t waitpid(pid_t pid, int *status, int options);
-
wait方法
-
作用:会阻塞父进程(只有接收到子进程退出信息父进程后面的代码才会执行),直到任意一个子进程退出。
status
参数是用于获取子进程的退出状态的。 -
返回值:成功返回被获取的子进程的PID,失败返回-1。
-
waitpid方法
-
作用:相较于wait方法waitpid方法更加精准高效
-
PID > 0
: 它可以设置传入参数pid用于指定等待哪个子进程 -
pid = -1
:等待任意子进程(类似wait
)。 -
pid = 0
:等待与父进程同进程组的所有子进程。 -
pid < -1
:等待进程组 ID 为|pid|
的所有子进程。
-
-
options
参数(常用选项)-
0
:默认阻塞模式。 -
WNOHANG
:非阻塞模式,立即返回。若无子进程退出,返回0
-
-
返回值
-
成功:返回被回收的子进程 PID。
-
失败:返回
-1
(如无子进程)。 -
WNOHANG
且无退出子进程:waitpid
返回0
。
-
区别:
4.2 status参数
这个参数由子进程的退出码和退出信号决定,如果在该参数处传入NULL/nullptr,表示不关心子进程退出状态信息,这个status参数由OS输出填充。
我们要通过status获取退出码可以使用位运算:
[ouyang@iZ2ze0j6dd76e0o9qypo2rZ dir_2025_2_13]$ ./process
我是一个子进程!我是父进程,我接收到退出码:8
或者可以使用宏解析:
-
WIFEXITED(status)
:子进程是否正常退出。 -
WEXITSTATUS(status)
:获取子进程退出码。 -
WIFSIGNALED(status)
:子进程是否被信号终止。 -
WTERMSIG(status)
:获取终止子进程的信号。
5.进程替换
-
什么是进程替换:将一个进程的代码和数据替换成另一个进程!
-
进程替换一旦成功,后续就执行新的代码和数据。
RETURN VALUE The exec() functions return only if an error has occurred. The return value is -1, and errno is set to indicate the error.
-
进程替换不会生成新的进程只是将老进程的代码和数据覆盖式的替换了。
-
进程替换使用的是exec*系列函数,这类函数无需关系返回值,因为一旦返回代表失败!
进程替换案例:
[ouyang@iZ2ze0j6dd76e0o9qypo2rZ dir_2025_2_13]$ ./process
代码程序开始了!
total 24
drwxrwxr-x 2 ouyang ouyang 4096 Feb 13 14:45 .
drwxrwxr-x 14 ouyang ouyang 4096 Feb 13 13:56 ..
-rwxrwxr-x 1 ouyang ouyang 8496 Feb 13 14:45 process
-rw-rw-r-- 1 ouyang ouyang 824 Feb 13 14:45 process.c
5.1 认识exec*系列函数
-
l(list):表示参数采用列表
-
v(vector):参数用数组
-
p(path):有p自动搜索环境变量PATH
-
e(env):表示自己维护环境变量
-
传参方面:1. 路径+程序名 2. 命令怎么写就怎么输入
// 库函数
#include <unistd.h>
extern char **environ; // 全局环境变量指针
// 需要传入程序的具体地址,执行参数,NULL表示参数结尾
int execl(const char *path, const char *arg, ...);
// 只需要传入程序名字,根据环境变量查找
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[]);
// 只需要传入程序名字,需要传入参数数组,不能直接传入参数
int execvp(const char *file, char *const argv[]);
// 只需要传入程序名字,需要传入参数数组,不能直接传入参数,环境变量可以不使用默认全局的,可以自己配置
int execvpe(const char *file, char *const argv[], char *const envp[]);
// 系统调用
#include <unistd.h>
int execve(const char *filename, char *const argv[], char *const envp[]);
库函数是对系统调用进行的函数封装!
-
execl
int main()
{
printf("Start\n");
execl("/bin/ls", "-a", "-l", NULL); // NULL表示传参结束
printf("End\n");
return 0;
}
[ouyang@iZ2ze0j6dd76e0o9qypo2rZ dir_2025_2_13]$ ./process
Start
total 16
-rwxrwxr-x 1 ouyang ouyang 8496 Feb 13 14:56 process
-rw-rw-r-- 1 ouyang ouyang 984 Feb 13 14:56 process.c
-
execlp(提供环境变量)
int main()
{
printf("Start\n");
execlp("ls", "-a", "-l", NULL); // NULL表示传参结束
printf("End\n");
return 0;
}
-
execv (自己提供argv[])
char* const argv[]{
"ls",
"-a",
"-l",
NULL
};
int main()
{
printf("Start\n");
execv("ls", argv); // NULL表示传参结束
printf("End\n");
return 0;
}
-
如果需要在原本的基础上新增环境变量使用putevn() 环境变量
SYNOPSIS
#include <stdlib.h>
int putenv(char *string);
6.自定义Shell
设计图:重定向方面的实现(参考I/O操作)
#include <iostream>
#include <cstdio>
#include <cstring>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <fcntl.h>
// 基本信息PutName大小
#define PUTSIZE 1024
// 获取命令行变量Command大小
#define COMMANDSIZE 1024
// 打印格式
#define PUTFOEM "[%s@%s %s] # "
// 我们的环境变量表
#define MAX_ENVS 120
char* g_env[MAX_ENVS];
int g_envs = 0;
// 我们的命令行参数表
#define MAX_ARGV 128
char* g_argv[MAX_ARGV];
int g_argc = 0;
// 实时获取cwd
char cwd[1024];
// 更新env的cwd
char cwdenv[1024];
// 上一次的退出码
int lastcode = 0;
// 4. 关于重定向,我们关心的内容
#define NONE_REDIR 0 // 无
#define INPUT_REDIR 1 // <
#define OUTPUT_REDIR 2 // >
#define APPEND_REDIR 3 // >>
int redir = NONE_REDIR;
std::string filename;
/* 获取父进程的环境变量表 */
void Init_Env()
{
extern char** environ; // 全局环境变量表
memset(g_env, 0, sizeof(g_env)); // 初始化我们的环境变量表
g_envs = 0;
/* 获取父进程环境变量表的内容 */
for(int i = 0; environ[i]; i++)
{
// 1. 申请空间
g_env[i] = (char*)malloc(strlen(environ[i]) + 1);
// 2. 拷贝内容
strcpy(g_env[i], environ[i]);
g_envs++;
}
// 标志
g_env[g_envs++] = (char*)"MyShell = Ouyang Test";
// 最后结尾以NULL
g_env[g_envs] = NULL;
/* 导入环境变量 */
for(int i = 0; g_env[i]; ++i)
putenv(g_env[i]);
// 更改指向
environ = g_env;
}
/* 测试环境变量是否导入成功 */
void Print_env()
{
extern char** environ;
for(int i = 0; environ[i]; ++i)
printf("%d->%s\n", i, environ[i]);
}
/* 用户名 / 主机名 / 绝对路径 / 家目录 */
const char* Get_User()
{
const char* GU = getenv("USER");
return GU == NULL ? "None" : GU;
}
const char* Get_Hostname()
{
const char* GH = getenv("HOSTNAME");
return GH == NULL ? "None" : GH;
}
const char* Get_PWD()
{
// const char* GP = getenv("PWD");
const char* GP = getcwd(cwd, sizeof(cwd));
if(GP != NULL)
{
snprintf(cwdenv, sizeof(cwdenv), "PWD=%s", cwd);
putenv(cwdenv);
}
return GP == NULL ? "None" : GP;
}
const char* Get_Home()
{
const char* Home = getenv("HOME");
return Home == NULL ? "None" : Home;
}
const char* Get_Oldpwd()
{
const char* old_pwd = getenv("OLDPWD");
return old_pwd == NULL ? "None" : old_pwd;
}
/* 获取相对路径 */
std::string DirName(const char* pwd)
{
#define SLASHE "/"
std::string rp = pwd;
if(rp == SLASHE) return SLASHE;
/* 找到最后的"/"位置 */
auto pos = rp.rfind(SLASHE);
if(pos == std::string::npos) return "Get P_Path Fail!";
return rp.substr(pos + 1);
}
/* 基本信息的打印 */
void PutBaseInformation()
{
fflush(stdout);
char PutName[PUTSIZE];
const char* pwd = Get_PWD();
snprintf(PutName, sizeof(PutName), PUTFOEM, Get_User(), Get_Hostname(), DirName(pwd).c_str());
printf("%s", PutName);
fflush(stdout);
}
/* 获取命令行 */
bool GetCommandLine(char* out, int size)
{
// "ls -a -l \n" -> "ls -a -l"
if(fgets(out, size, stdin) == nullptr) return false;
out[strlen(out) - 1] = 0; // 清理"/n"
return strlen(out) > 0; // 没有命令输入不做处理
}
/* 解析命令 */
bool CommandPrase(char* out)
{
// "ls -a -l" -> "ls" "-a" "-l"
// 以" "为分隔符
#define SPACE " "
g_argc = 0; // 重置
g_argv[g_argc++] = strtok(out, SPACE);
while((bool)(g_argv[g_argc++] = strtok(NULL, SPACE)));
g_argc--; // 减去多余的++
return g_argc > 0;
}
/* 打印测试g_argv */
void Print_Argv()
{
for(int i = 0; g_argv[i]; ++i)
printf("g_argv[%d]->%s", i, g_argv[i]);
}
/* Cd */
void Cd()
{
// Cd / Cd - / Cd ~
if(g_argc == 1)
{
// Cd
std::string home = Get_Home();
if(home.empty()) return;
chdir(home.c_str());
}
else{
// Cd - / Cd ~
std::string op = g_argv[1];
if(op == "~")
{
// 家目录
chdir(Get_Home());
}
else if(op == "-")
{
// 之前目录
chdir(Get_Oldpwd());
}
else{
chdir(op.c_str());
}
}
}
/* Echo */
void Echo()
{
if(g_argc == 2 && !(g_argc > 2))
{
// echo $?
// echo "Hello"
// echo $PATH
std::string opt = g_argv[1];
if(opt == "$?")
{
std::cout << lastcode << std::endl;
lastcode = 0;
}
else if(opt[0] == '$')
{
std::string env_name = opt.substr(1);
char* env = getenv(env_name.c_str());
if(env)
std::cout << env << std::endl;
}
else{
std::cout << opt << std::endl;
}
}
}
/* 特殊处理内建命令 */
bool CheckAndExecBuiltin()
{
std::string cmd = g_argv[0];
if(cmd == "cd")
{
Cd();
return true;
}
else if(cmd == "echo")
{
Echo();
return true;
}
// ...
return false;
}
void TrimSpace(char cmd[], int &end)
{
while(isspace(cmd[end]))
{
end++;
}
}
bool CheckRedirect()
{
redir = NONE_REDIR;
filename.clear();
for (int i = 0; g_argv[i] != NULL; ++i)
{
if (strcmp(g_argv[i], "<") == 0)
{
redir = INPUT_REDIR;
if (g_argv[i + 1] != NULL)
{
filename = g_argv[i + 1];
g_argv[i] = NULL; // 截断参数列表
return true;
}
return false;
}
else if (strcmp(g_argv[i], ">") == 0)
{
redir = OUTPUT_REDIR;
if (g_argv[i + 1] != NULL)
{
filename = g_argv[i + 1];
g_argv[i] = NULL; // 截断参数列表
return true;
}
return false;
}
else if (strcmp(g_argv[i], ">>") == 0)
{
redir = APPEND_REDIR;
if (g_argv[i + 1] != NULL)
{
filename = g_argv[i + 1];
g_argv[i] = NULL; // 截断参数列表
return true;
}
return false;
}
}
return false;
}
/* 命令执行 */
int Execute()
{
// 进程创建 + 进程替换
pid_t id = fork();
if(id == 0)
{
int fd = -1;
// 子进程检测重定向情况
if(redir == INPUT_REDIR)
{
fd = open(filename.c_str(), O_RDONLY);
if(fd < 0) exit(1);
dup2(fd, 0);
close(fd);
}
else if(redir == OUTPUT_REDIR)
{
umask(0);
fd = open(filename.c_str(), O_CREAT | O_WRONLY | O_TRUNC, 0666);
if(fd < 0) exit(2);
dup2(fd, 1);
close(fd);
}
else if(redir == APPEND_REDIR)
{
umask(0);
fd = open(filename.c_str(), O_CREAT | O_WRONLY | O_APPEND, 0666);
if(fd < 0) exit(2);
dup2(fd, 1);
close(fd);
}
// child
execvp(g_argv[0], g_argv);
exit(EXIT_FAILURE);
}
// father
// 进程等待
int st = 0;
pid_t pid = waitpid(id, &st, 0);
lastcode = WEXITSTATUS(st);
return pid;
}
int main()
{
/* 初始化我们的环境变量 */
Init_Env();
// Print_env(); /* 测试Env */
while(true)
{
/* 基本信息的打印 */
PutBaseInformation();
/* 命令行的获取 */
char CommandLine[COMMANDSIZE];
if(!GetCommandLine(CommandLine, sizeof(CommandLine)))
continue;
/* 解析命令 */
if(!CommandPrase(CommandLine))
continue;
/* 重定向分析 */
if(CheckRedirect())
{
Execute();
continue;
}
/* 特殊处理内建命令 */
if(CheckAndExecBuiltin())
continue;
// 命令执行
Execute();
}
return 0;
}