解密进程管理:从创建到回收全解析(基于Liunx操作系统)
1.进程
1.1程序和进程的区别
程序:代码或者指令的有序堆叠,是静态的,存放在磁盘空间。
进程:程序的运行实例,进程是动态的。
1.2单道程序和多道程序
--单道程序:所有进程一个一个排队执行。若 A 阻塞,B 只能等待,即使 CPU 处于空闲状态。而在人机交互时阻塞的出现是必然的。所有这种模型在系统资源利用上及其不合理,在计算机发展历史上存在不久,大部分便被淘汰了。(例如一开始的dos系统)
--多道程序:在计算机内存中同时存放几道相互独立的程序,它们在管理程序控制之下,相互穿插的运行。
1.3并行和并发
并行(一定是多核):多道程序同时在多个处理器上运行。
并发:宏观上的并行,并不是真正的并行,而是基于多道程序设计的一种根据时间片来回切换cpu使用权的一种特殊机制。
1.4进程控制块(PCB:process-control-bloc)
进程运行时,内核为进程每个进程分配一个 PCB(进程控制块),维护进程相关的信息,Linux 内核的进程控制块是 task_struct 结构体。
查看task_struct 结构体:
在终端输入命令:
结果:
pcb
2.进程的状态
2.1进程的三种状态
就绪状态:准备完毕,等待执行
运行状态:获得CPU资源,正在使用
等待状态:当有事件发生,条件不满足,需要重新准备(也有叫阻塞态的,这里的说法是基于Liunx系统,是为学习嵌入式。)
2.2进程状态的查看
ps可以查看进程信息
可以结合的命令行参数:
-a 显示终端上的所有进程,包括其他用户的进程
-u 显示进程的详细状态
-x 显示没有控制终端的进程
-w 显示加宽,以便显示更多的信息
-r 只显示正在运行的进程
-ajx 更深层次的进程信息
ps - aux 相当于linux里面的任务管理器
3.进程号
3.1什么是进程号
每个进程都由一个进程号(pid)来标识,其类型为 pid_t(整型),进程号的范围:0~32767( 2¹⁵)。进程号总是唯一的,但进程号可以重用。当一个进程终止后,其进程号就可以再次使用。
两个特殊进程:
- 0进程:
- 早期叫做:交换进程
- 现在叫做:空闲进程(当CPU空闲的时候,会执行这个进程)
- 1进程:init进程,是所有用户进程的祖先(所有用户进程都是有1号进程直接或者间接创建)。(1号进程由0号进程创建)
pid:进程的编号
ppid:父进程,每一个进程都是由父进程创建而来的。ppid就是父进程的编号。
pgid:进程组,一个或者多个进程的集合,这些进程的集合称为进程组,进程组有一个组号,叫做pgid。这些进程相互关联,可以接收终端传来的各种信号。
3.2获取进程的函数
getpid
获取当前进程的进程号
#include <sys/types.h>
#include <unistd.h>
pid_t getpid(void);
返回值:获取的当前进程的进程号,pid_t
参数:无参数
getppid
#include <sys/types.h>
#include <unistd.h>
pid_t getppid(void);
返回值:获取的当前进程的父进程号,pid_t
参数:无参数
getpgid
#include <sys/types.h>
#include <unistd.h>
pid_t getpgid(pid_t pid);
返回值:获取的pid进程的进程组号,pid_t
参数:需要传入要找的pid号
案例:
#include<stdio.h>
#include<sys/types.h>
#include<unistd.h>int main(int argc, char const *argv[])
{pid_t pid = getpid();printf("pid:%d\n",pid);pid_t ppid = getppid();printf("ppid:%d\n",ppid);pid_t pgid = getpgid(pid);printf("pgid:%d\n",pgid);while (1){ }return 0;
}
结果:
4.进程的创建
4.1进程的创建函数fork函数:
进程的创建,由fork函数来完成。
#include <sys/types.h>
#include <unistd.h>pid_t fork(void);
函数功能:创建一个新的进程。这个被创建的新的进程叫做子进程,是调用fork函数这个进程(父进程)的子进程。
返回值:
如果创建成功:
- 在父进程中返回的是子进程的id
- 在子进程中返回的是0
如果创建失败:
- 返回-1
4.2父子进程的关系(重点)
pid ==0 是子进程
pid >0 是父进程
子进程是父进程的复制品(从fork的下一句开始复制),两个进程都有各自单独的空间。
所以最终执行终端打印两个结果,一个是在父进程中进行,一个是在子进程中进行。只是在同一个终端显示。
子进程从父进程处继承了整个进程的地址空间。 地址空间: 包括进程上下文、进程堆栈、打开的文件描述符、信号控制设定、进程优先级、进程组号等。 子进程所独有的只有它的进程号,计时器等。因此,使用 fork 函数的代价是很大的。
案例:
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>int main(int argc, char const *argv[])
{pid_t pid = fork();if (pid == -1){perror("fork");return 0;}else if (pid > 0){printf("hello,welcome to parent process\n");printf("当前进程id%d,他的子进程id%d\n", getpid(), pid);}else if (pid == 0){printf("hello,welcome to child process\n");printf("子进程pid:%d,他的父进程id:%d\n", getpid(), getppid());}return 0;
}
拓展:
sleep函数
#include <unistd.h>
unsigned int sleep(unsigned int seconds);
函数功能:将进程挂起(休眠/暂停)一段时间。
返回值:
- 如果sleep函数里面的seconds走完了,那么返回0秒
- 如果没有休眠完,被强制打断,那么返回剩余待休眠的时间
参数:挂起或者休眠的时间
案例
每隔一秒打印一次i
全缓冲与行缓冲
全缓冲:缓冲区满了以后强制进行刷新,即write函数可以将数据输出到终端
行缓冲:遇到\r\n,进行刷新,此时write函数可以将数据输出到终端
vfork函数(了解,不要使用,内存风险高)
函数原型:
#include <sys/types.h>
#include <unistd.h>
pid_t vfork(void);
函数功能:创建一个新的进程。这个被创建的新的进程叫做子进程,是调用vfork函数这个进程(父进程)的子进程。
与fork的区别:
- fork是子进程复制父进程的内容,两者相互独立。
- vfork创建的进程,父子共享内存,不独立进行。先执行子进程,再执行父进程。
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
int main(int argc, char const *argv[])
{int num =0;pid_t pid = vfork();if (pid == -1){perror("vfork");return 0;}else if (pid > 0){while (1){ sleep(1);printf("当前进程id%d,他的子进程id%d\n", getpid(), pid);printf("num=%d\n",num);_exit(0);}}else if (pid == 0){while (1){sleep(1);num = 1000;printf("num=%d\n",num);printf("当前进程id%d,他的父进程id%d\n", getpid(), getppid());_exit(0);}}return 0;
}
exit函数 与 _exit函数
exit函数:
功能:库函数,将进程相关数据 清理后退出
除了调用_exit函数还会调用一个清理函数
#include <stdlib.h>
void exit(int status);
_exit函数:
功能:系统调用函数,将进程直接退出,不进行清理
void _exit(int status);
5.进程资源回收
在每个进程退出的时候,内核释放该进程所有的资源、包括打开的文件、占用的内存等。但是仍然为其保留一定的信息,这些信息主要主要指进程控制块 PCB 的信息(包括进程号、退出状态、运行时间等)。 父进程可以通过调用 wait 或waitpid 得到它的退出状态同时彻底清除掉这个进程。
5.1wait函数
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status)
函数功能:等待任意一个子进程结束,如果任意一个子进程结束了,此函数会回收该子进程的资源。
参数:
status : 进程退出时的状态信息,是一个指针
返回值:
- 成功:返回运行结束的进程的进程号
- 失败:-1
代码案例:
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h>int main(int argc, char const *argv[])
{pid_t pid = fork();if (pid == -1){perror("fork");return 0;}else if (pid == 0){for (int i = 1; i <= 5; i++){printf("我还能再玩%ds\n", 5 - i);sleep(1);}_exit(0);}else if (pid > 0){int status = 0;printf("你5s以后必须停止玩游戏\n");sleep(5);wait(&status); //阻塞,等待子进程退出if (WIFEXITED(status) != 0){printf("status:%d\n", WEXITSTATUS(status));if (WEXITSTATUS(status)==0){printf("好的,你说的对,听你的\n");}else if (WEXITSTATUS(status)==1){printf("好的,你说的不对,但是我还是听你的\n");}}}return 0;
}
补充:WIFEXITED WEXITSTATUS
WIFEXITED(status)
功能:检查子进程是否正常退出。
说明:
这是一个条件判断宏,它接收
wait()
或waitpid()
得到的status
作为参数。返回值:
如果子进程是正常退出的(例如调用了
exit()
、_exit()
函数,或者从main()
函数return
),则该宏返回一个非零值(真)。如果子进程是因为其他原因终止的(例如被信号杀死),则该宏返回 0(假)。
WIFEXITED
用来回答“这个进程是自己正常结束的吗?”这个问题。
WEXITSTATUS(status)
功能:获取子进程正常退出时的退出状态码。
参数
这个宏也接收
status
作为参数。返回值:
它只有在
WIFEXITED(status)
返回为真(非零)时使用才有意义!它从
status
中提取出子进程传给exit()
或return
的退出码(状态)(一个 0-255 之间的整数)。就是exit(num)
、_exit(num)
中的num(自己赋值)如果进程是正常退出的,
WEXITSTATUS
用来回答“它退出时返回的状态是多少?”这个问题。先使用
WIFEXITED
判断退出类型,如果是正常退出,再使用WEXITSTATUS
获取退出状态。
5.2waitpid函数
函数原型:
pid_t waitpid(pid_t pid, int *wstatus, int options);
函数功能:如果子进程终止了,此函数会回收子进程的资源(可以阻塞也可以不阻塞)。
参数:
pid:
- pid > 0 回收进程ID等于 pid 的子进程。
- pid = 0 回收同一个进程组中的任何子进程,如果子进程已经加入了别的进程组,waitpid 不会等待它。
- pid = -1 回收任一子进程,此时 waitpid 和 wait 作用一样。
- pid < -1 回收指定进程组中的任何子进程,这个进程组的 ID 等于 pid 的绝对值。(例如:传进去-9527,其实是指定9527这个进程组)
wstatus:与wait里面的status用法一致
options:
- 0:同 wait(),阻塞父进程,等待子进程退出(阻塞)。
- WNOHANG:没有任何已经结束的子进程,则立即返回(不阻塞)。
- WUNTRACED:如果子进程暂停了则此函数马上返回,并且不予以理会子进程的结束状态。(由于涉及到一些跟踪调试方面的知识,加之极少用到)
返回值:
1) 当正常返回的时候,waitpid() 返回收集到的已经回收子进程的进程号;
2) 如果设置了选项 WNOHANG,而调用中 waitpid() 发现没有已退出的子进程,则返回 0;
3) 如果调用中出错(比如没有在运行的子进程了),则返回-1,可以用perror打印错误信息。
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h>int main(int argc, char const *argv[])
{pid_t pid = fork();if (pid == -1){perror("fork");return 0;}else if (pid == 0){for (int i = 1; i <= 5; i++){printf("我还能再玩%ds\n", 5 - i);sleep(1);}_exit(0);}else if (pid > 0){int ret = 0;while (1){ret = waitpid(pid,NULL,WNOHANG);if (ret > 0){break;}}printf("ret = %d\n",ret);}return 0;
}
6.特殊进程
6.1僵尸进程
进程结束,但是相关资源没有被回收,就会变成僵尸进程。僵尸进程是有危害的,因为占用pid资源,而且pid资源有限。
代码案例:
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h>int main(int argc, char const *argv[])
{pid_t pid = fork();if (pid == -1){perror("fork");return 0;}else if (pid == 0){for (int i = 1; i <= 5; i++){printf("我还能再玩%ds\n", 5 - i);sleep(1);}}else if (pid > 0){while (1) //父进程没有对子进程的相关资源进行回收,子进程编程僵尸进程//如果父进程也结束了,那么他们被系统回收{}}return 0;
}
查看:
6.2孤儿进程
父进程运行结束,但子进程还在运行(未运行结束)的子进程就称为孤儿进程(Orphan Process)。无危害,因为系统(1号进程)会对孤儿进程进行资源回收。
6.3守护进程(精灵进程)
守护进程(Daemon Process),也就是通常说的 Daemon 进程(精灵进程),是 Linux 中的后台服务进程。它是一个生存期较长的进程,通常
独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。 守护进程是个特殊的孤儿进程,这种进程脱离终端,
7.多进程的创建(重点)
请你帮我创建两个子进程:
错误的写法:
创建了两个子进程,但是第一个子进程又创建了一个“孙子”进程,
fork :子进程是父进程的复制品(从fork的下一句开始复制)
父进程中
第一次循环fork后 创建 第二个子进程
在子进程中
第一个子进程 (此时i=1)还可以循环一次,也就是可以fork一个“孙子”进程,然后i++,不满足i<2停止
父进程中
第二次循环fork后 创建 第二个子进程
在子进程中
第二个子进程 (此时i=2),不满足i<2停止
8.终端
ttyname函数
#include <unistd.h>char *ttyname(int fd);
函数功能:该函数检查文件描述符 fd 是否连接到一个终端设备。如果是,则返回一个指向字符串的指针,该字符串包含该终端设备的路径名(例如
/dev/pts/0 或 /dev/tty1)。
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <sys/wait.h>int main(int argc, char const *argv[])
{printf("%s\n",ttyname(0));printf("%s\n",ttyname(1));printf("%s\n",ttyname(2));return 0;
}
终端:
在 UNIX 系统中,用户通过终端登录系统后得到一个 Shell 进程,这个终端成为Shell 进程的控制终端(Controlling Terminal)。
1.当shell进程想通过./a.out的形式启动一个进程,其实是通过fork以及exec函数族共同作用,然后会把终端控制权交给a.out
2.如果这个a.out再创建一个子进程,那么这个子进程也拥有终端控制权。
3.如果a.ou即,父进程先结束,那么啊a.out的子进程将丢失终端的控制权,将控制权重新还给shell进程
进程组
进程组:进程组,一个或者多个进程的集合,这些进程的集合称为进程组,进程组有一个组号,叫做pgid。这些进程相互关联,可以接收终端传来的各种信号。一般父进程创建一个子进程的时候,他们两个就自动归到一个进程里面。一般fork函数得到的子进程都与父进程在一个进程组,但是shell终端的fork除外。
前台进程组:获得终端读写权限的进程组。
后台进程组:只有终端写权限的进程组
会话
会话:会话是一个或多个进程组的集合,一个会话中的几个进程组可被分为一个前台进程组以及一个或多个后台进程组;
如果一个PID == PGID == SID那么可以说这个进程是会长
创建新会话:
1) 调用进程不能是进程组组长,该进程变成新会话首进程(session header)
2) 该调用进程是组长进程,则出错返回
3) 该进程成为一个新进程组的组长进程
4) 需有 root 权限(ubuntu 不需要)
5) 新会话丢弃原有的控制终端,该会话没有控制终端 (精灵&守护进程)
6) 建立新会话时,先调用 fork, 父进程终止,子进程调用 setsid
getsid函数:
#include <unistd.h>
pid_t getsid(pid_t pid);
功能:获取进程所属的会话 ID
参数:
pid:进程号,pid 为 0 表示查看当前进程 session ID
返回值:
成功:返回调用进程的会话 ID
失败:-1
组长进程不能成为新会话首进程,新会话首进程必定会成为组长进程。
setsid函数
#include <unistd.h>
pid_t setsid(void);
功能:创建一个会话,并以自己的 ID 设置进程组 ID,同时也是新会话的 ID。调用了 setsid 函数的进程,既是新的会长,也是新的组长。
案例
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <sys/wait.h>int main(int argc, char const *argv[])
{pid_t pid = fork();if (pid < 0){perror("fork");}else if (pid ==0 ) //子进程{setsid(); //脱离原来的会话,变成新会话while(1){sleep(1);printf("我在默默守护你\n");} }else if (pid > 0) //父进程{_exit(0); }return 0;
}
9.exec函数族(不需要记,理解即可,需要时查)
在winodws下,启动一个程序,双击.exe文件。如果在linux下,./a.out。如果我想在一个进程里启动另一个进程呢?这里我们通过 exec 函数族实现。
exec 函数族,顾名思义,就是一簇函数。
#include <unistd.h>
extern char **environ;
int execl(const char *path, const char *arg, .../* (char *) NULL */);
int execlp(const char *file,cconst char *arg, ... /* (char *) NULL */);
int execle(const char *path, const char *arg, .../*, (char *) NULL, 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[]);
int execve(const char *filename, char *const argv[], char *const envp[]);
p:以 p 结尾的 exec 函数取文件名做为参数。当指定 filename 作为参数时若 filename 中包含/,则将其视为路径名,并直接到指定的路径中执行程序。
e:e(environment): 存有环境变量字符串地址的指针数组的地址。
代码案例1:
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <sys/wait.h>
int main(int argc, char const *argv[])
{printf("hello exec!\n");execl("/bin/ls", "-a",NULL);printf("welcome the next time\n");return 0;
}
注意:exec 函数族取代调用进程的数据段、代码段和堆栈段。所以,exec函数后面的代码被新启动的进程覆盖,不再执行。
代码案例2:
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <sys/wait.h>
int main(int argc, char const *argv[])
{printf("hello exec!\n");//execl("bin/ls", "-a","-l",NULL);execlp("ls", "-a","-l",NULL); //带有p,即带有环境参数printf("welcome the next time\n");return 0;
}