1、进程是什么?
进程 处于执行期的程序(可执行程序代码 + 与之相关被使用到的各种资源),相对于程序,它的特点是动态的,时时变化的。
上面提到的资源,抽象的来说:如打开的文件、挂起的信号、处理器状态、内存地址空间等等。
从另外一个角度,Linux进程一般包括代码段、数据段、堆栈段。代码段存放程序的可执行代码,是共享的;数据段存放程序的全局变量、常量、静态变量;堆存放动态分配的内存变量;栈用于函数调用,存放函数参数、函数内部定义的局部变量。
2、进程在内核中的存放和表示
由 slab分配器 动态生成。在内核栈的尾端会分配 thread_info结构体, 在结构体中,有相应的task_struct结构体的指针。
位于/arch/arm/include/asm/thread_info.h
13 struct thread_info {
14 struct pcb_struct pcb; /* palcode state */
15
16 struct task_struct *task; /* main task structure */
17 unsigned int flags; /* low level flags */
18 unsigned int ieee_state; /* see fpu.h */
19
20 struct exec_domain *exec_domain; /* execution domain */
21 mm_segment_t addr_limit; /* thread address space */
22 unsigned cpu; /* current CPU */
23 int preempt_count; /* 0 => preemptable, <0 => BUG */
24
25 int bpt_nsaved;
26 unsigned long bpt_addr[2]; /* breakpoint handling */
27 unsigned int bpt_insn[2];
28
29 struct restart_block restart_block;
30 };
在linux中,进程往往通过task_struct这个结构体来进行管理。此结构体又称为进程描述符,(有和进程相关的所有信息,有1.7K左右大小);
对于每个进程,都有PID(进程ID)与之相对应,该信息存储在tast_struct中。
通过current宏来查找正在运行的进程。(此查找宏,和体系结构有关,架构不同,该宏的实现也不同)
寄存器多的处理器架构通过单独的寄存器存放进程描述符,加快速度;
而寄存器较少的处理器往往就通过thread_info结构体来后的task_struct;
3、进程的状态
五种:
Task_running
可运行的任务(包括准备好的和正在运行的,进程在用户空间中执行的唯一可能的状态)
Task_interruptable
正在睡眠或堵塞的任务,等待某些条件的达成。达成后,变为准备运行状态;
Task_uninterruptable
不会因为接收到信号而复活,别的和上面的差不多;
TASK_UNINTERRUPTIBLE状态存在的意义就在于,内核的某些处理流程是不能被打断的。如果响应异步信号,程序的执行流程中就会被插入一段用于处理异步信号的流程(这个插入的流程可能只存在于内核态,也可能延伸到用户态),于是原有的流程就被中断了.
在进程对某些硬件进行操作时(比如进程调用read系统调用对某个设备文件进行读操作,而read系统调用最终执行到对应设备驱动的代码,并与对应的物理 设备进行交互),可能需要使用TASK_UNINTERRUPTIBLE状态对进程进行保护,以避免进程与设备交互的过程被打断,造成设备陷入不可控的状态。
Task_traced
被其他进程跟踪的进程
Tast_stopped
进程停滞执行。这种状态发生在接收到SIGSTOP/SIGTSTP/SIGTTIN/SIGTTOU等信号的时候。
特殊的两种状态
进程终止才会拥有的状态,即可存放在state中,也可存放在exit_state中
EXIT_ZOMBIE
被终止,但是父进程还没有调用wait4或者wait_pid来获取死亡子进程的信息;
EXIT_DEAD
父进程已经调用,正在销毁信息的过程中。
进程的初始状态
进程是通过fork系列的系统调用(fork、clone、vfork)来创建的,内 核(或内核模块)也可以通过kernel_thread函数创建内核进程。这些创建子进程的函数本质上都完成了相同的功能——将调用进程复制一份,得到子 进程。(可以通过选项参数来决定各种资源是共享、还是私有。)
那么既然调用进程处于TASK_RUNNING状态(否则,它若不是正在运行,又怎么进行调用?),则子进程默认也处于TASK_RUNNING状态。
另外,在系统调用调用clone和内核函数kernel_thread也接受CLONE_STOPPED选项,从而将子进程的初始状态置为 TASK_STOPPED。
3.2.4 进程状态的改变(state在task_struct中)
Set_task_state(task, state) 和直接的tast->state是有区别的(比如是否设置内存屏障,防止重排)
set_current_state(state)和set_task_state(current, state)
4、进程间关系
Linux进程之间存在明显的继承关系;
在linux中,所有的进程都是PID为1的init的进程的后代(有个问题,这个init进程究竟做了哪些事情);
进程间的关系主要有三种:
父进程、子进程、兄弟进程
Parent、children 、sibling 这些关系都保存在tast_struct中
进程的这些继承关系使系统可以从任何一个进程查找到任意指定的进程。
通过任一进程可以到达指定的其他的进程
举例:下一个
list_entry(指针,结构体,结构体的成员)
#define list_entry(1, 2, 3) container_of(1, 2, 3)
#define container_of(ptr, type, member) ({const typeof( ((type *)0)->member ) *__mptr = (ptr);
声明一个与Member同一个类型的指针变量并初始化为ptr
(type *)( (char *)__mptr - offsetof(type,member) );})
#define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)
得到地址,并作强制转换
还包括for_each_process(task)遍历所有进程等等;
5、进程相关信息的查询
通过ps命令来查询进程的相关信息;
或者是通过/proc下的某些文件
ps中的进程状态显示
S | 睡眠 |
R | 运行 |
D | 不可中断的睡眠 |
T | 停止 |
Z | 死亡进程或者说是僵尸进程 |
N | 低优先级任务 |
W | 分页 |
s | 进程是会话期首进程 |
+ | 进程属于前台进程 |
l | 进程是多线程的 |
< | 高优先级任务 |
6、进程的生命周期
概括为:fork() + exec() + exit()+ wait()
fork() 通过copy当前进程创建子进程(区别:PID不同,PPID不同,某些没必要的资源和统计量),也就是大部分原样copy;
exec() 读取可执行程序,将其载入地址空间,并开始运行。
exit() 释放大部分进程相关资源,并使该进程处于EXIT_ZOMBIE状态
wait() 使父进程得知子进程的死亡,并释放最后剩余的资源(包括内核栈、thread_info、task_struct)
6.1、进程的创建
6.1.1 概述
注意:以前的系统调用比如fork()、clone等,现在都改成了sys_fork、sys_clone
Linux的进程创建用的也是fork()函数,采用写时拷贝页(copy-on-right)来实现,内核此时并不复制整个进程地址空间,而是让父进程和子进程共享同一个拷贝(以只读方式)。
这种技术使地址空间上的拷贝推迟到实际发生写入的时候才进行(好处是显而易见的,当fork()后立马执行exec()时,大多数数据就无需进行复制了,节约了资源);
在这种情况下,父进程的开销:
复制父进程的页表(这里我不是很懂,这里的页表是指什么,里面有什么:一种解释:进程的页表就包括用户空间和内核空间的映射),以及给子进程创建唯一的进程描述符。
进程的创建有一组函数:如fork()、vfork()、__clone()
6.1.2 fork()的实现
主要是调用do_fork()系统调用来实现,定义于 /kernel/fork.c中
通过传递不同的参数标志来达到控制的目的:
CLONE_FLAGS
实现与/include/linux/sched.h中,可自行查看
6.1.3 fork()的使用
说明:在父进程中,fork返回的是新建子进程的PID;
在子进程中,fork返回的是0;
失败返回-1;
可由此判断当前处于子进程还是父进程。
实例:
输出:(子进程先运行。。。)
Fork program starting
This is the child
This is the parent
This is the parent
This is the child
This is the parent
This is the child
$This is the child
This is the child
6.2、新的执行镜像的载入
exec是一系列函数,来自于< unistd.h >;
exec将当前进程替换为新进程,进行了程序载入的工作;
+l list 变量列,是可变的;
+p 通过搜索PATH环境变量来查找新程序可执行文件的路径;
+e 传递环境变量envp
+v 传递数组
示例:
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
Int main()
{
printf(“Running ps with execlp\n”);
execlp(“ps”, “ps”, “ax”, 0);
printf(“done.\n”);
exit(0);
}
运行后,没有输出done,这是为什么呢?
因为exec是将新程序重新载入当前程序,PID/PPID/nice值等和之前的进程完全一样,完全继承,原来的程序已经不在了,当然不会打印done.
6.3、进程的消亡
6.3.1 概述
终结时,须释放相关资源,并通知父进程。
终结发生在exit()函数被调用时,有两种情况:
显式的调用;
当从某个程序的主函数返回时(C语言编译器会在主函数的返回点后面防止调用exit()的代码);
也有可能接收到它既不能处理也不能忽略的信号或异常时,可能会被被动的终结;但归根结底,最后的大部分工作是由do_exit来完成的。
6.3.2 do_exit()
do_exit()函数具体究竟做了些什么?
do_exit()函数的主要内容:
主要是释放相应的资源,比如内存、定时器,并恢复引用计数;
给子进程寻找新的父进程(通过exit_notify函数),并将此进程的状态改变为EXIT_ZOMBIE,
通过schedule()切换进程(这时,与进程相关的资源已被释放,还被占用的就是内核栈、thread_info、 task_struct结构体)
do_exit函数之后,线程处于僵死状态:
Zombie状态,进程存在的唯一目的就是告诉父进程,自己已死。父进程知道后,释放最后的信息。
6.3.3 僵死状态的进程
当子进程终止时,它与父进程的联系还会保持,它的基本信息还会保存在内存中,直到父进程正常终止或者父进程调用wait函数。在完全注销之前,该子进程叫做僵尸进程。(defunct或者zombie)
如果父进程异常终止,则该僵尸子进程会把PID为1的init进程作为父进程,在Init发现该僵尸进程并清除之前(init将如何发现挂靠在自己之下的僵尸进程),他将一直占用资源。
解决办法:
wait函数或者waitpid(等待特定进程的结束)
#include <sys/types.h>
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *stat_loc, int options)
若pid参数为-1,则等待任意进程;
Stat_loc 接受状态信息,并用宏做判断;
Option:控制选项
返回:子进程没有结束或意外终止(如果意外终止,该进程的结果会如何,它所占资源系统会如何处置),返回0;
否则返回-1;
waitpid失败,返回-1并设置errno(包括没有子进程ECHILD、被某个信号中断EINTR、选项参数无效EINVAL)