基础知识
- Linux内核为每个进程分配了PCB(task_struct),并加入对应的列表,对进程的管理,其实就是对PCB、PCB链表的操作
- 增加:
- 分配PCB内存,并填入相应信息
- 复制父进程资源(写时才复制)
- 加入多个链表,如全局链表、调度队列等
- 删除:
- 发送终止信号(如
SIGTERM)- 释放资源
- 从所有链表中移除对应的PCB
- 父进程通过wait()回收僵尸进程
- 修改:更新PCB内容
- 查询:从链表查找对应指定进程的PCB
- PCB记录着程序的信息,如:
- 进程ID:唯一标识一个进程。
- 状态:记录当前进程的运行状态(如就绪、运行、等待等)。
- 优先级:用于进程调度,优先级高的进程更容易获得CPU时间。
- 内存指针:指向进程的代码段、数据段、栈段。
- 程序计数器:记录下一条需要执行的指令的地址。
- I/O状态信息:列出与该进程相关的I/O设备和文件。
- PCB会处于多个链表中,且每个链表都是双向链表;在PCB中嵌入多个链表(如下代码),每次操作PCB都需要操作多个链表
struct list_head { struct list_head *next, *prev; }; struct task_struct { // 进程的其他字段 struct list_head tasks; // 所有进程的链表 struct list_head children;// 子进程链表 struct list_head sibling; // 兄弟进程链表 struct list_head ptrace_list; // ptrace 跟踪链表 // 更多链表节点... }; - 万物皆文件:所有进程都被管理在虚拟文件系统/proc/(本身不占据磁盘空间)之下,如pid=1234的进程就对应/proc/1234,里面记录着进程所有信息,如文件打开表记录在/proc/1234/fd/之下,里面存储着链接文件,命名就是文件描述符(句柄),如下所示:
ps指令就是去读取/proc/中的文件,并提取需要输出的信息,按照格式进行输出
- 僵尸进程(defunct):进程已经终止,但是父进程没有调用wait()/waitpid()回收资源,PCB仍保留在链表中
- 孤儿进程:父进程先总是,子进程此时仍在运行,且由init(1号)进程接管,init进程一直都在运行wait(),等待回收孤儿进程
僵尸进程-->孤儿进程:当主进程退出都没有回收僵尸进程的资源,此时僵尸进程会被init接管变为孤儿进程,进而被回收;所以ps看见的僵尸进程(状态为z)都是父进程没有结束而导致的。
- 进程组:多个进程的集合,每个进程都有唯一的PGID,通常为组长进程的PID,子进程会继承父进程的PGID,只有组内所有进程都消失才算该组结束。
cmd1 | cmd2其实就是一个进程组
- 会话:多个进程组的集合;当有新的用户登录Linux时,登录进程会为这个用户创建一个会话。用户的登录shell就是会话的首进程。会话的首进程ID会作为整个会话的ID。会话是一个或多个进程组的集合,囊括了登录用户的所有活动。
会话和进程组都是为了方便管理终端,当然还有创建守护进程也与之密切关系
基本函数
-
pid_t
getpid(void); -
pid_t
getppid(void);- 获取父进程号
- 在终端中使用./启动程序时,此时终端bash就是该进程的父进程
-
int
setpgid(pid_t pid, pid_t pgid);- 修改自身或子进程的PGID
- 子进程执行exec()后就不能修改其PGID
-
pid_t
getpgid(pid_t pid);- 获取组号,默认为父进程pid
-
pid_t
setsid(void);- (不能在组长进程中调用)只能在子进程中调用
- 子进程完全独立出来,与终端不再是一个会话,从而脱离终端控制
- 调用后,子进程变成新会话首进程以及新进程组组长
-
pid_t
fork(void);- 变量(堆)读时共享,写时拷贝
- 如果共享代码中分配了堆,那子进程、父进程中都需要释放
valgrind ./可执行文件,来查看内存泄漏
-
void
exit(int status);- 参数是返回给父进程的参数,低8位有效
- 终结该进程,并回收资源(除了pcb),如堆栈代码段、文件描述符等
- 会刷新io缓冲区,将内存中的内容写入文件中
-
void
_exit(int status);- 功能一样,但是是直接杀死进程,并清理资源
- io缓冲区的内容直接清理
-
pid_t
wait(int *status);- 父进程阻塞等待子进程退出,若没有子进程则立即返回,用于回收子进程的pcb并获取状态
- 传入的参数用于接受子进程调用exit传出的参数,status需要搭配成套宏函数使用,来判断返回的状态
- 返回退出的子进程pid
-
pid_t
waitpid(pid_t pid,int *status,int options);- 和wait差不多,只是wait是阻塞等待,waitpid可配置
- 参数说明
- pid>0 等待指定进程
- pid=0 等待同一组中任何进程
- pid=-1 等待任一子进程
- pid<-1 等待指定进程组中任何子进程,进程组为pid绝对值
- status 和wait中一样
- options=0 阻塞等待
- options=WNOHANG:没有任何已经结束的子进程,则立即返回
- 返回说明:
-
0 子进程正常退出
- =0 非阻塞模式,子进程正在运行
- <0 子进程异常退出或已经没有子进程
-
-
int status; pid_t pid; while ((pid = waitpid(-1, &status, WNOHANG)) == 0) { //轮询等待回收资源 printf("无子进程退出,父进程继续执行其他任务\n"); sleep(1); }
status
- 父进程无法直接读取子进程中的变量,子进程也无法直接写入父进程变量中,子进程只能写入os分配的空间,然后父进程调用wait、waitpid将数据移动到自己的变量中
- status是按位写入的,整体的指并没有作用
- 低7位记录信号,比如kill -9 pid,就会记录信号为9
- 低第8位没有使用
- 次低8位:进程退出时所对应的退出状态(退出码)
printf("waitpid返回的退出码:%d,信号:%d", status >> 8 & 0xFF, status & 0x7F); -
进程替换函数
exec族函数参考链接
调用成功后,就使用指定的程序来替换当前进程,新老进程,就代码、数据不同,且会从新程序的main从头执行,老程序的代码全部抛弃,包括没有执行的部分 调用失败,则出错返回-1exec函数族分别是:execl, execlp, execle, execv, execvp, execvpe,规律如下:
- l(list) : 表示参数采用列表,NULL作为参数结束标志;
- v(vector) : 参数用数组;
- p(path) : 有p自动搜索环境变量PATH,所以第一个参数只用指定名字就行,而不需要路径; - e(env) : 表示自己维护环境变量;execl("/usr/bin/ls","ls","-a","-l",NULL); execlp("ls","ls","-a","-l",NULL); //第一个ls代表路径,需要用此去环境变量中查找路径 //第二个ls代表在命令行中如何调用此指令(程序) char* const argv[] = { "ls", "-a", "-l", NULL }; execv("/usr/bin/ls",argv); execvp("ls",argv);
创建守护进程
ps ajx
步骤
- 创建子进程,父进程退出
- 子进程中创建新会话
setsid()- 子进程脱离控制
- 改变当前目录位置(一般改为根目录下)
chdir()- 因为守护进程是一直运行的,所以不能占用可卸载的文件系统(如U盘)
- 重设文件权限掩码
umask(八进制)- 默认的文件为666,目录为 777
- 实际权限 = 默认权限 & (~umask)
- 如umask(0); // 权限掩码设为0,文件默认权限为666 & ~0 = 666,目录为777 & ~0 = 777
- 关闭文件描述符(0、1、2一般是重定向)
- 继承的打开文件不会用到,浪费系统资源
- 且由于脱离了终端,那标准输入输出就无法使用
close()、open("/dev/null",O_RDWR)、dup2(newfd,oldfd);for (int i = 0; i < sysconf(_SC_OPEN_MAX); i++) { close(i); } int fd = open("/dev/null", O_RDWR); dup2(fd, STDIN_FILENO); // 标准输入 dup2(fd, STDOUT_FILENO); // 标准输出 dup2(fd, STDERR_FILENO); // 标准错误
- 执行核心工作