文章目录
前言
在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; }