文章目录
前言
一、进程的概念与结构
1. 相关概念
2. 内核区中的进程结构
3. 进程的状态
4. 获取进程ID函数
二、进程创建
1. fork和vfork函数
2. 额外注意点
3. 构建进程链
4.构建进程扇
三、进程终止
1. C程序的启动过程
2. 进程终止方式
四、特殊的进程
1. 僵尸进程
2. 守护进程
3. 孤儿进程
五、相关函数
1. wait函数
2. waitpid函数
3. execl函数
4. execlp函数
前言
在Linux中程序的运行涉及进程的相关知识,熟悉并掌握其相关知识在嵌入式Linux应用开发中至关重要。本篇记录进程的具体知识,若涉及版权问题,请联系本人删除!
一、进程的概念与结构
1. 相关概念
- 程序:存放在硬盘的可执行文件。
- 进程:是程序运行的实例,每个进程都有一个虚拟地址空间。进程之间相互独立,同时也存在相关机制来进行进程的通信。每个Linux进程都有唯一的进程ID(PID),其都是正整数。
- 并发:虚假的同时运行多个进程,是单CPU切换速度极快的结果。
- 并行:真实的同时运行多个进程,有多个CPU。
- 命令:①如下图,通过命令"ps -aux"可以查看进程信息。②用kill -9可以强制退出进程。
2. 内核区中的进程结构
每启动一个进程,在虚拟地址空间的内核区中就会对应一个task_struct结构体(进程控制块PCB),如下图所示。其中包含了进程的ID、状态、优先级、调度策略、文件结构体指针(指向文件描述符表)等等。
3. 进程的状态
有五种常见状态:创建态、就绪态、运行态、阻塞态(挂起态)和退出态(终止态)。
- 创建态:进程在创建时就是该状态,时间很短。
- 就绪态:创建后就处于该状态,等待抢夺CPU时间片。
- 运行态:获得CPU资源使得该进程运行,当时间片用完后重新回到就绪态。
- 阻塞态:进程强制放弃CPU,无法抢夺CPU时间片(例如sleep在休眠期间)。同时,阻塞态又分为不可中断和可中断类型。(执行中按下Ctrl+C能中断的是可中断类型)
- 退出态:进程的终止,占用的系统资源被释放。(任何状态都可以直接转换为退出态)
僵尸状态:进程已经终止了,用户区资源已经被释放了,但是内核区中的task_struct仍有信息,ps的命令中STAT值为Z。
4. 获取进程ID函数
#include <unistd.h>
#include <sys/types.h>
当前进程ID: pid_t getpid(void);
当前进程的父进程ID: pid_t getppid(void);
当前进程的实际用户ID: uid_t getuid(void);
当前进程的有效用户ID: uid_t geteuid(void);
当前进程的用户组ID: gid_t getgid(void);
当前进程的进程组ID: pid_t getpgrp(void);
进程ID为pid的进程组ID: pid_t getpgid(pid_t pid);
【注】实际用户是当前环境下的用户,有效用户是真正开启进程的用户
二、进程创建
1. fork和vfork函数
【1】头文件:#include <sys/types.h>、#include <unistd.h>
【2】函数原型:①pid_t fork(void); ②pid_t vfork(void);
【3】功能:
- fork创建子进程,且子进程复制父进程的内存空间。子、父进程谁先运行看进程调度。
- vfork创建子进程,子进程先运行且不复制父进程空间。
2. 额外注意点
- fork和vfork被调用一次,会返回两次:子进程中的返回值为0,在父进程中的返回值则是子进程的PID。可以根据返回值不同来区分是父进程还是子进程。
- 失败返回值:创建子进程失败会返回-1。
- 执行位置:父进程是从main函数代码体首部开始执行,子进程是从fork函数之后开始执行。
- 虚拟地址空间的用户空间:子进程中代码段与环境变量的物理空间和父进程是同一个。而其他的物理空间不是同一个(而是将父进程的复制一份给子进程),即使它们的虚拟地址是一样的。
- 虚拟地址空间的内核空间:①子进程只复制父进程的文件描述符表,不复制但共享文件表项和inode。②父进程创建一个子进程后,文件表项中的引用计数器加1,当父进程close后计数器减1,子进程还是可以使用文件表项,只有当计数器为0时才会释放文件表项。
实验程序1:创建子进程,打印子、父进程中的pid信息。
#include <stdio.h> #include <sys/types.h> #include <unistd.h> int main(int argc, char **argv) { //fork创建子进程,复制父进程空间 pid_t pid = fork(); //子、父进程中打印pid if (pid < 0) { perror("创建子进程失败"); } else if (pid == 0) {//子进程 printf("I am child process. PID: %d, PPID: %d, 返回的PID: %d\n", getpid(), getppid(), pid); } else {//父进程 printf("I am parent process. PID: %d, PPID: %d, 返回的PID: %d\n", getpid(), getppid(), pid); } return 0; }
实验程序2:父进程将文件指针定位到文件尾部,子进程写入内容。原有目录下有文件1.txt,原有内容为123
#include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> #include <string.h> int main(int argc, char **argv) { //命令行参数判定 if (argc != 2) { printf("Command: %s <filename>\n", argv[0]); return -1; } //文件操作 int fd = open(argv[1], O_WRONLY); if (fd < 0) { perror("文件打开错误"); return -1; } //父进程改变文件指针到文件尾部 //子进程等待父进程定位好后写入内容 pid_t pid = fork(); if (pid < 0) { perror("创建子进程错误"); close(fd); return -1; } else if (pid > 0) {//父进程 if (lseek(fd, 0, SEEK_END) < 0) { perror("文件指针定位错误"); close(fd); return -1; } } else {//子进程 sleep(2);//确保父进程先运行 const char * content = "Hello, Can!\n"; int contentSize = strlen(content); if (write(fd, content, contentSize) < contentSize) { printf("写入错误\n"); close(fd); return -1; } } printf("--------pid: %d完成工作---------\n", getpid()); //关闭文件:父子进程都会关闭,使得引用计数减为0 close(fd); return 0; }
3. 构建进程链
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main(int argc, char **argv)
{
//创建3个子进程,形成进程链
for (int i = 0; i < 3; ++i) {
pid_t pid = fork();
if (pid < 0) {
perror("创建失败");
return -1;
}
if (pid > 0) { //若为父进程则退出
break;
}
}
printf("PID: %d, PPID: %d\n", getpid(), getppid());
sleep(1);
return 0;
}
4.构建进程扇
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main(int argc, char **argv)
{
//创建3个子进程,形成进程扇
for (int i = 0; i < 3; ++i) {
pid_t pid = fork();
if (pid < 0) {
perror("创建失败");
return -1;
}
if (pid == 0) {//若为子进程则退出
break;
}
}
printf("PID: %d, PPID: %d\n", getpid(), getppid());
sleep(1);
return 0;
}
三、进程终止
1. C程序的启动过程
在main函数执行前,Linux内核会启动一个特殊例程,将命令行中的参数传给argc和argv。若主函数中有三个形参,那么该例程还会将环境信息构建成环境表传给第三个形参。最后,该例程还会登记进程的终止函数(进程终止前会调用)。
终止函数说明:
- 每个进程都默认登记了一个标准的终止函数。
- 终止函数在进程终止时释放一些资源。
- 登记的多个终止函数的执行顺序按照栈的方式执行。
- 用户自定义终止函数(无参无返回值),需要调用atexit函数向内核登记。
atexit函数:
【1】头文件:#include <stdlib.h>
【2】功能:向内核登记一个终止函数,该函数会在正常进程终止时被调用。
【3】函数原型:int atexit(void (*function)(void));
【4】返回值:成功返回0,否则返回非零值。
2. 进程终止方式
- 正常终止:
- ①main函数中return返回 会刷新标准IO缓存,会执行自定义的终止函数
- ②调用库函数exit(0) 会刷新标准IO缓存,会执行自定义的终止函数
- ③调用系统调用函数_exit(0)或_Exit(0) 不会刷新标准IO缓存,不会执行自定义的终止函数
- ④最后一个线程从其启动例程返回
- ⑤最后一个线程调用库函数pthread_exit
- 异常终止:
- ①调用库函数abort
- ②接收到信号并终止(例如段错误会产生一个信号,然后终止进程)
- ③最后一个线程对取消请求做处理响应
实验程序:运行下列代码,若参数指定为exit或return,文件中有写入的字符串,并且会执行自定义的终止函数;若参数指定为_exit,文件中没有任何内容,并且没有执行终止函数。
#include <stdio.h> #include <string.h> #include <stdlib.h> #include <unistd.h> //自定义终止函数 void fun1() { printf("Terminate: fun1\n"); } void fun2() { printf("Terminate: fun2\n"); } void fun3() { printf("Terminate: fun3\n"); } //主函数 int main(int argc, char **argv) { //命令行参数判定 if (argc != 3) { printf("commnd: %s <filename> <exit | return | _exit>\n", argv[0]); return -1; } //登记自定义终止函数 atexit(fun1); atexit(fun2); atexit(fun3); //文件操作,忽视健壮性判定 FILE *fd = fopen(argv[1], "w");//文件不存在则创建,调用失败返回NULL fprintf(fd, "Hello, world!\n");//向文件缓冲区写入字符串,若没有刷新或fclose则不会写入硬盘 //根据参数选择退出方式 if (!strcmp(argv[2], "exit")) { exit(0); } else if (!strcmp(argv[2], "return")) { return 0; } else { _exit(0); } }
四、特殊的进程
1. 僵尸进程
- 概念:子进程的虚拟地址空间中的用户区资源已经释放,但内核区中的task_struct没有被释放,那么该进程就是僵尸进程。
- 释放僵尸进程的方式:
- ①结束或kill僵尸进程的父进程,那么僵尸进程就会成为孤儿进程,然后会被init进程(1号进程)领养,最终会被回收。
- ②让僵尸进程的父进程来回收。父进程每隔一段时间就查询子进程是否结束并回收,调用wait函数或waitpid函数,通过内核来释放僵尸进程。
- ③采用信号SIGCHLD通知处理,在信号处理函数中调用wait函数。
程序示例:运行如下程序,就会生成僵尸进程。
#include <stdio.h> #include <sys/types.h> #include <unistd.h> int main(int argc, char **argv) { //创建子进程 pid_t pid = fork(); if (pid < 0) { perror("创建子进程失败"); return -1; } //子进程退出,成为僵尸进程 if (pid == 0) { printf("PID: %d, PPID: %d\n", getpid(), getppid()); return -1; } //父进程循环,便于观察 while(1) { sleep(1); } return 0; }
2. 守护进程
- 概念:是一种生存期很长的进程。从操作系统启动开始,在操作系统关闭时终止。
- 所有守护进程都以root(用户ID为0)的优先权运行。
- 守护进程没有控制终端,一直在后台运行。
- 守护进程的父进程都是init进程。
3. 孤儿进程
- 概念:父进程结束了,但是子进程还在运行,那么此时子进程就是孤儿进程。孤儿进程由init进程(1号进程)来回收。
- 领养机制引入:进程的用户区资源可以自己释放,但是内核区资源需要由父进程释放。而孤儿进程的父进程已经结束。因此,为了释放孤儿进程的内核区资源,让1号进程来领养它,进而释放其内核区的task_struct结构体。
程序示例:通过fork创建子进程,同时让父进程退出,那么子进程就是孤儿进程。
#include <stdio.h> #include <sys/types.h> #include <unistd.h> int main(int argc, char **argv) { //创建子进程 pid_t pid = fork(); if (pid < 0) { perror("创建子进程失败"); return -1; } //父进程退出 if (pid > 0) { printf("PID: %d, PPID: %d\n", getpid(), getppid()); return -1; } //子进程成为孤儿进程 if (pid == 0) { sleep(2); printf("PID: %d, PPID: %d\n", getpid(), getppid()); return -1; } return 0; }
五、相关函数
1. wait函数
【1】头文件:#include <sys/types.h>、#include <sys/wait.h>
【2】函数原型:pid_t wait(int *wstatus);
【3】参数说明:wstatus是传出的参数,存放子进程退出时的信息。例如:wait(7status);
取出整形变量status中的数据需要使用一些宏函数:
- WIFEXITED(status)用于判定是否是正常结束,是的话返回真;WEXITSTATUS(status)取出对应的进程退出码。
- WIFSIGNALED(status)用于判定是否是异常结束,是的话返回真;WTERMSIG(status)取出对应的进程退出码。
- WIFSTOPPED(status)用于判定是否是暂停子进程的返回,是的话返回真;WSTOPSIG(status)取出对应的进程退出码。
【4】功能:父进程等待子进程退出并回收,避免僵尸进程和孤儿进程产生。
【5】返回值:成功则返回子进程的PID,失败返回-1
【6】注意:wait函数等待所有的子进程退出。
示例程序:演示子进程异常退出,父进程对退出码进行处理。
#include <stdio.h> #include <sys/types.h> #include <sys/wait.h> #include <unistd.h> #include <stdlib.h> int main(int argc, char **argv) { //创建子进程 pid_t pid = fork(); if (pid < 0) { perror("创建子进程失败"); return -1; } //子进程:打印信息,异常退出 if (pid == 0) { printf("PID: %d, PPID: %d\n", getpid(), getppid()); int i = 3, j = 0, k = i/j;//由于除0异常退出 } //父进程:阻塞等待子进程退出,将退出码保存 int status; pid_t ret = wait(&status); if (ret < 0) { printf("回收失败\n"); } else { printf("回收成功,子进程PID:%d\n", ret); } //父进程:处理退出码 if(WIFEXITED(status)) { printf("正常退出:%d\n", WEXITSTATUS(status)); } else if (WIFSIGNALED(status)) { printf("异常退出:%d\n", WTERMSIG(status)); } else if (WIFSTOPPED(status)) { printf("暂停退出:%d\n", WSTOPSIG(status)); } else { printf("未知退出\n"); } return 0; }