unistd.h头文件中pid_t getpid(void);返回调用进程的PIDpid_t getppid(void);返回调用进程的父进程的PIDuid_t getuid(void);返回调用进程的实际用户IDgid_t getgid(void);返回调用进程的实际组IDuid_t geteuid(void);返回调用进程的有效用户IDgid_t getegid(void);返回调用进程的有效组ID创建子进程的函数包含在unistd.h头文件中
fork函数
pid_t fork(void);
fork函数会失败。创建子进程示例代码
#include
#include
int main(void)
{
printf("haha\n");
// 创建子进程
int pid = fork();
printf("heihei\n");
return 0;
}
/*
haha
heihei
heihei
*/
以下是父子进程中数据相关copy的示例图

验证上图
#include
#include
#include
#include
int global = 100; // 父进程全局变量->数据区
int main(void)
{
int local = 200; // 父进程局部变量->栈区
int *heap = malloc(sizeof(int)); // 动态分配内存->堆区
*heap = 3;
printf("父进程第一次打印: PID->%d %p->%d %p->%d %p->%d\n", getpid(), &global, global, &local, local, heap, *heap);
// 创建子进程
pid_t pid = fork();
if(pid == 0)
{
// 子进程操作,数据会从父进程copy一份过来,这里执行++操作
printf("子进程打印: PID->%d PPID->%d %p->%d %p->%d %p->%d\n", getpid(), getppid(), &global, ++global, &local, ++local, heap, ++*heap);
return 0;
}
sleep(1); // 这里等1s,让子进程++
printf("父进程第二次打印: PID->%d %p->%d %p->%d %p->%d\n", getpid(), &global, global, &local, local, heap, *heap);
return 0;
}
/*
父进程第一次打印: PID->1674604 0x5577e1acc010->100 0x7ffd4d4bfaa8->200 0x5577e23422a0->3
子进程打印: PID->1674605 PPID->1674604 0x5577e1acc010->101 0x7ffd4d4bfaa8->201 0x5577e23422a0->4
父进程第二次打印: PID->1674604 0x5577e1acc010->100 0x7ffd4d4bfaa8->200 0x5577e23422a0->3
这里的父进程和子进程地址一样是虚拟地址里面一样,因为每个进程都有一个独立的虚拟地址池,相互不影响的
发现子进程跟父进程互相不影响,验证了上图的案例
*/
父子进程操作文件,其实是共享一个文件表项的

验证上图
#include
#include
#include
#include
#include
int main(void)
{
// 父进程打开文件
int fd = open("./test.txt", O_WRONLY | O_CREAT | O_TRUNC, 0664);
if(fd == -1)
{
perror("open");
return -1;
}
// 父进程写入数据
char *data = "hello bhlu!";
if(write(fd, data, strlen(data)) == -1)
{
perror("write");
return -1;
}
// 创建子进程
pid_t pid = fork();
if(pid == 0)
{
// 子进程修改文件读写位置
if(lseek(fd, -5, SEEK_END) == -1)
{
perror("lseek");
return -1;
}
return 0;
}
// 再次插入数据,验证子进程修改的读写位置是否生效
sleep(1); // 先等1s,让子进程执行完
data = "linux\n";
if(write(fd, data, strlen(data)) == -1)
{
perror("write");
return -1;
}
// 关闭文件
close(fd);
return 0;
}
/*
cat test.txt
hello linux
发现是修改成功的,说明上图是对的,子进程和父进程共用一个文件表项
*/
以下内容只是简单的介绍进程的终止,以便理解
进程的终止分为两种
正常终止:分为三种情况
main函数中正常返回
使用exit函数终止:exit函数可以在任何函数中执行令进程结束,return语句只有在main函数中执行才能令进程结束
#include
void exit(int status);
/*
功能: 令进程终止
参数: status 进程的退出码,相当于main函数的返回值
无返回值
*/
/*
exit函数在终止前会做以下几件收尾工作
1. 调用实现通过atexit或on_exit函数注册的函数退出函数
2. 冲刷并关闭所有仍处于打开状态的标准I/O流
3. 删除所有通过tmpfile函数创建的临时文件
4. 执行_exit(status);
使用exit函数令进程终止,通常使用EXIT_SUCCESS和EXIT_FAILUR两个宏
EXIT_SUCCESS -> 1; EXIT_FAILUR -> 0;
*/
调用_exit/_Exit函数令进程终止
// _exit函数
#include
void _exit(int status);
/*
参数: status 进程的退出码,相当于main函数的返回值
无返回值
*/
// _Exit函数
#include
void _Exit(int status);
/*
参数: status 进程的退出码,相当于main函数的返回值
无返回值
*/
/*
_exit函数在终止前会做以下几件收尾工作
1. 关闭所有仍处于打开状态的文件描述符
2. 将调用进程的所有子进程托付过init进程
3. 向调用进程的父进程发送SIGCHLD(7)信号
4. 令调用进程终止运行,将status的低八位作为退出码保存在其终止状态中
*/
异常终止
进程执行了系统认为具有危险性的操作时,或者系统本身发生故障或意外,内核会向进程发送特定的信号
SIGILL(4) -> 进程试图执行非法指令
SIGBUS(7) -> 硬件或对齐错误
SIGEPE(8) -> 浮点异常
SIGSEGV(11) -> 无效内存访问
SIGPWR(30) -> 系统供电不足
人为触发信号
SIGINT(2) -> Ctrl+c
SIGQUIT(3) -> Ctrl+\
SIGKILL(9) -> 不能被捕获或忽略的进程终止信号
SIGTERM(15) -> 可以被捕获或忽略的进程终止编号
向进程自己发送信号
#include
void abort(void);
/*
功能: 想进城发送SIGABRT(6)信号,该信号默认情况下可以使进程结束
无返回值
*/
在使用
exit函数或main函数正常退出时,如果注册了atexit或on_exit,那就会触发退出函数,以下是示例代码#include#include void func(void) { exit(6); } void goto1(void) { printf("goto1\n"); } void goto2(int status, void *arg) { printf("status = %d\n", status); printf("arg = %s\n", (char *)arg); } int main(void) { atexit(goto1); // 退出之前执行goto1 on_exit(goto2, "heihei"); // 退出之前执行goto2,可以传参 func(); return 0; } /* 相当于钩子函数,在退出之前执行,可以进行一些回收操作 status = 6 arg = heihei goto1 */
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
wait函数是用于回收子进程的一个函数,它使用的是阻塞回收,使用它必须包含sys/wait.h头文件
wait函数
pid_t wait(int *status);
功能:等待和回收任意子进程
参数:status用于输出子进程的终止状态,可置NULL
补充:可以使用以下工具宏分析子进程的终止状态
if(WIFEXITED(status))
// 真
printf("正常终止: 进程退出码是%d\n", WEXITSTATUS(status));
else
// 假
printf("异常终止: 终止进程的信号是%d\n", WTERMSIG(status));
// 下面跟上面判断条件相反
if(WIFSIGNALED(status))
// 真
printf("异常终止: 终止进程的信号是%d\n", WTERMSIG(status));
else
// 假
printf("正常终止: 进程退出码是%d\n", WEXITSTATUS(status));
返回值:成功返回回收的子进程PID,失败返回-1
简单代码示例
// 子进程的回收
#include
#include
#include
#include
int main(void)
{
// 创建子进程
pid_t pid = fork();
if(pid == -1)
{
perror("fork");
return -1;
}
// 子进程相关操作
if(pid == 0)
{
printf("%d进程: 我是子进程!\n", getpid());
// sleep(5);
// exit(3);
// _exit(5);
// return 2;
// abort(); // 向进程发送信号异常结束
// 以下两句会造成内存无效访问,会返回11
char *p = NULL;
*p = 123;
}
// 父进程等待回收子进程
printf("%d进程: 我是父进程!\n", getpid());
int s; // 用来输出所回收的子进程终止状态
pid_t childpid = wait(&s);
if(childpid == -1)
{
perror("wait");
return -1;
}
printf("父进程回收了%d进程的僵尸!\n", childpid);
// 根据返回值判断子进程是否是正常结束
if(WIFEXITED(s))
printf("正常结束: %d\n", WEXITSTATUS(s));
else
printf("异常结束: %d\n", WTERMSIG(s));
return 0;
}
以下代码是一个循环创建5个进程,然后父进程挨个回收
#include
#include
#include
#include
int main(void)
{
printf("%d进程: 我是父进程!\n--------------------------\n", getpid());
sleep(1);
// 创建子进程
for(int i = 0; i < 5; i++)
{
pid_t pid = fork();
if(pid == -1)
{
perror("fork");
return -1;
}
// 子进程操作
if(pid == 0)
{
printf("%d进程: 我是子进程!\n", getpid());
sleep(i+1);
return i+1;
}
}
// 父进程操作: 回收子进程
while(1)
{
int s; // 用户接收子进程的终止状态
pid_t childpid = wait(&s);
if(childpid == -1)
{
if(errno == ECHILD)
{
printf("没有子进程可以回收了!\n");
break;
}
else
{
perror("wait");
return -1;
}
}
// 判断子进程的终止状态
if(WIFEXITED(s))
printf("正常结束: %d\n", WEXITSTATUS(s));
else
printf("异常终止: %d\n", WTERMSIG(s));
}
return 0;
}
waitpid函数一般用于非阻塞回收子进程,还可以回收特定子进程,使用这个函数需要引用sys/wait.h头文件
waitpid函数
pid_t waitpid(pid_t pid, int *status, int options);
pid:取-1等待并回收任意子进程,相当于wait函数,>0等待回收特定子进程status:用于输出子进程的终止状态,可置NULLoption:0代表阻塞模式,WNOHANG代表非阻塞模式,如果等待的进程还在运行,则返回0以下是使用非阻塞回收的方法回收子进程
#include
#include
#include
#include
#include
int main(void)
{
printf("%d进程: 我是父进程!\n-------------------------\n", getpid());
// 创建子进程
for(int i = 0; i < 5; i++)
{
pid_t pid = fork();
if(pid == -1)
{
perror("fork");
return -1;
}
// 子进程相关操作
if(pid == 0)
{
printf("%d进程: 我是子进程!\n", getpid());
sleep(i+1);
// 三种效果
if(i == 3)
{
abort();
}
else if(i == 4)
{
char *p = NULL;
*p = 123;
}
else
{
return i+1;
}
}
}
// 父进程回收子进程
sleep(1);
while(1)
{
int s; // 用于保存进程的终止状态
pid_t childpid = waitpid(-1, &s, WNOHANG); // 这里使用的是非阻塞模式
if(childpid == -1)
{
// 报错或者没有子进程了
if(errno == ECHILD)
{
printf("没有子进程了!\n");
break;
}
else
{
perror("waitpid");
return -1;
}
}
else if(childpid == 0)
{
// 子进程还在运行
printf("子进程在运行,无法回收,先睡会!\n");
sleep(2);
}
else
{
// 回收成功并判断是否正常终止
printf("%d子进程回收成功!\n", childpid);
if(WIFEXITED(s))
printf("%d进程正常终止, 进程退出码: %d\n\n", childpid, WEXITSTATUS(s));
else
printf("%d进程异常终止, 终止进程信号: %d\n\n", childpid, WTERMSIG(s));
}
}
return 0;
}
/*
代码执行效果
1761797进程: 我是父进程!
-------------------------
1761798进程: 我是子进程!
1761799进程: 我是子进程!
1761800进程: 我是子进程!
1761801进程: 我是子进程!
1761802进程: 我是子进程!
子进程在运行,无法回收,先睡会!
1761798子进程回收成功!
1761798进程正常终止, 进程退出码: 1
1761799子进程回收成功!
1761799进程正常终止, 进程退出码: 2
子进程在运行,无法回收,先睡会!
1761800子进程回收成功!
1761800进程正常终止, 进程退出码: 3
1761801子进程回收成功!
1761801进程异常终止, 终止进程信号: 6
子进程在运行,无法回收,先睡会!
1761802子进程回收成功!
1761802进程异常终止, 终止进程信号: 11
没有子进程了!
*/
与fork函数不同,这里使用的exec函数是创建一个新的进程,新进程会取代调用自身的进程,新进程覆盖之前的进程地址空间,进程的PID不会改变。

exec不是一个函数,而是一堆函数,功能一样,用法相似
#include
int execl(const char *path, const char *arg, ...);
execl("/bin/ls", "ls", "-a", "-l", NULL);
/*
path使用的是路径名
使用NULL作为arg的结尾
失败返回-1,成功不返回
*/
int execlp(const char *file, const char *arg, ...);
execlp("ls", "ls", "-a", "-l", NULL);
/*
file使用的是文件名,会从环境变量中一个个的找
使用NULL作为arg的结尾
失败返回-1,成功不返回
*/
int execle(const char *path, const char *arg, ..., char *const envp[]);
char *envp[] = {"NAME=bhlu", "AGE=25", NULL};
execle("/usr/bin/env", "env", NULL, envp);
/*
比excel多一个envp,用于设置环境变量,它设置什么,新进程的环境变量就只有什么
失败返回-1,成功不返回
环境变量输出:
NAME=bhlu
AGE=25
*/
int execv(const char *path, char *const argv[]);
char *argv[] = {"ls", "-a", "-l", NULL};
execv("/bin/ls", argv);
/*
execv系列使用的都是字符指针数组,字符数组是以NULL结尾
失败返回-1,成功不返回
*/
int execvp(const char *file, char *const argv[]);
char *argv[] = {"ls", "-a", "-l", NULL};
execvp("ls", argv);
/*
跟execv差不多,就第一个参数是文件名
失败返回-1,成功不返回
*/
int execve(const char *path, char *const argv[], char *const envp[]);
char *argv[] = {"env", NULL};
char *envp[] = {"NAME=bhlu", "AGE=25", NULL};
execve("/usr/bin/env", argv, envp);
/*
跟execle函数差不多,就是这里的第二个参数是字符指针数组
失败返回-1,成功不返回
*/
后缀不同,代码的含义也不同
l:即list,新进程的命令以字符指针列表形式传入,列表以空指针结束p:即path:第一个参数,不包含/,就根据PATH环境变量搜索文件e:即environment:设定环境变量,不指定则从调用进程复制v:即vector:新进程的命令行参数以字符指针数组的形式传入,数组以空指针结束execve函数使用exec函数基本会将原进程的所有信号、属性、数据等都丢失或者恢复初识状态,只有PID、PPID、UID等会被继承下来。
一般都会先创建一个子进程,然后在子进程中使用exec函数,以下是相关示例
#include
#include
int main(void)
{
// 创建子进程
pid_t pid = fork();
if(pid == -1)
{
perror("fork");
return -1;
}
// 子进程相关操作
if(pid == 0)
{
char *argv[] = {"env", NULL};
if(execvp("/bin/env", argv) == -1)
{
perror("execvp");
return -1;
}
}
// 父进程操作
printf("父进程PID: %d\n", getpid());
return 0;
}
下面介绍的是c语言执行shell命令的函数
#include
int system(const char *command);
shell命令shell命令,如果参数取NULL,返回非0表示Shell可用,返回0表示不可用command进程的终止状态, 失败返回-1代码实例
#include
#include
#include
int main(void)
{
int s = system("echo $PATH");
if(s == -1)
{
perror("system");
return -1;
}
printf("父进程PID: %d\n", getpid());
return 0;
}
system函数内部调用了vfork、exec和waitpid等函数,而且它是标准库函数,可以跨平台使用
vfork或waitpid函数出错,则返回-1exec函数出错,则在子进程中执行exit(127)waitpid获取command进程的终止状态