经常被问到进程和线程的区别,今天对进程进行详细的分析讨论。
一、进程的定义
进程是操作系统的概念,每当我们执行一个程序时,对于操作系统来讲就创建了一个进程,在这个过程中,伴随着资源的分配和释放。可以认为进程是一个程序的一次执行过程。
二、进程与程序的区别
程序时静态的,它是一些保存 在磁盘上得指令的有序集合,没有任何执行的概念。
进程是一个动态的概念,它是程序执行的过程,包括创建、调度和消亡。
三、linux系统中进程的表示
在linux系统中,进程由一个叫task_struct的结构体描述,也就是说linux中的每个进程对应一个task_struct结构体。该结构体记录了进程的一切。下面我们来看看它的核心字段。
struct task_struct { //这个是进程的运行状态,-1代表不可运行,0代表可运行,>0代表已经停止。 volatile long state; /* flags是进程当前的状态标志,具体如下: 0x0002表示进程正在被创建 0x0004表示进程正准备退出 0x0040表示此进程被fork出,但是并没有执行exec 0x0400表示此进程由于其他进程发送相关信号而被杀死 */ unsigned int flags; //反应进程状态的信息,但不是运行状态 //表示此进程的运行优先级 unsigned int rt_priority; //该结构体记录了进程内存使用的相关情况 struct mm_struct *mm; //进程号,是进程的唯一标识 pid_t pid; //进程组号 pid_t tgid; //real_parent是该进程的"亲生父亲",不管其是否被"寄养"//该指针指向创建当前进程的的进程,或者是当创建者已经消亡后,该指针指向init进程。 struct task_struct *real_parent; //parent是该进程现在的父进程,有可能是"继父"//一般情况下和real_parent一样,只是当另外一个进程调用了ptrace()系统调用后 struct task_struct *parent; //这里children指的是该进程孩子的链表,可以得到所有孩子的进程描述符 struct list_head children; //同理,sibling该进程兄弟的链表,也就是其父进程的所有孩子的链表 struct list_head sibling; //这个是主线程的进程描述符,也许你会奇怪,为什么线程用进程描叙符表示,因为linux并没有单独实现线程的相关结构体,只用一 个进程来代替线程,然后对其做一些特殊的处理。 struct task_struct *group_leader; //这个是该进程所有线程的链表 struct list_head thread_group; //这个是该进程使用cpu时间的信息,utime是在用户态下执行的时间,stime 是在内核态下执行的时间 cputime_t utime,stime; //comm是保存该进程名字的字符数组,长度最长为15,因为TASK_COMM_LEN为16 char comm[TASK_COMM_LEN]; //打开的文件相关信息结构体 struct files_struct *files; //信号相关信息的句柄 struct signal_struct *signal; struct sigband_struct *sighand; };
四、linux进程中的文件
linux操作系统中每个进程有两个数据结构描叙文件相关信息
第一个:fs_struct,它包含此进程当前工作目录和根目录、umask。umask是新文件被 创建的缺省模式,它可以通过系统调用来改变。
第二个:files_struct,包含此进程正在使用的所有文件的信息。f_mode字段描述该文件是以什么模式创建的:只读、读写、还是只写。f_pos保存文件中下一个读或写将发生的位置。f_inode描叙文件的VFS索引节点,而f_ops是一个例程向量的指针,每个代表一个想施加于文件的操作的函数。
每次一个文件被打开时,files_struct中的空闲file指针之一就被用来指向新的file结构。Linux进程在启动时有三个文件描叙符被打开了,他们是标准输入设备、标准输出设备和标准错误设备,并且通常是从创建此进程的父进程继承得来的。所有对文件的访问时通过传递或返回文件描叙符的标准系统调用进行的。这些描述符是进程fd向量的索引,所以标准输入设备、标准输出设备和标准错误设备分别对应文件描述符0、1和2。
五、进程中的虚拟内存
在Linux操作系统中,当我们运行一个二级制可执行文件时,操作系统将创建一个进程。此时如果将这个可执行二进制文件的全部代码和数据装入物理内存将是浪费的。因为他们不可能同时使用。随着系统中进程数的增多,这种浪费将被成倍的扩大,系统将非常低效地运行。事实上,linux使用一种称为请求调页(demand-paging)的技术:只有当进程要使用时其虚拟内存时,其对应的数据才装入物理内存。所以,不是直接把代码和数据装入物理内存。linux内核只修改进程的页表,标识虚拟内存页存在但其对应的数据不在内存中。当进程想要访问代码或数据时,系统硬件将产生页故障并把控制交给Linux内核来解决。因此,对于进程地址空间中的每一个内存区,Linux都需要知道该虚拟内存来自何处,以及如何把它装入内存以解决故障。
当一个进程分配虚拟内存时,Linux并不真正为它保留物理内存。它只是创建一个新vm_area_struct数据结构来描叙虚拟内存,这个结构被链入进程的虚拟内存列表。当进程试图写一个位于新分配虚拟内存区域的虚拟地址时,系统将产生页故障。处理器试图转换该虚拟地址,但是因为没有此内存的页表项,它将放弃并产生一个页故障异常,留给Linux内核来解决。Linux查看被引用的虚拟地址是否是位于当前进程的虚拟内存地址空间。
如果是,Linux创建适当的PTE并为此进程分配一页物理内存。代码或数据可能需要从文件系统或交换硬盘上读入物理内存。然后进程可以从引起页故障的那条指令处重启,并且因为这次内存物理地址存在,所以它可以继续执行。
如果不是,就是大家常常见到的"段错误"。
下面来用一个程序加深对进程的了解,与上面的理论内容关系不是非常紧密,只是想说明进程创建的相互关系。
程序./20150814pstree.sh
./20150814pstree_sleep.sh & pstree -Aup ubuntu| grep -n -A2 -B3 ‘sh‘ while [ 0 ] do sleep 2 done
程序./20150814pstree_test.sh
echo "this is $0 ,my PID is $$" while [ 0 ] do # echo "3s has passed" sleep 3 done
Step1:程序运行结果如下:
gonme-terminal(2805)是本图形界面tty7对应的终端程序,tty7下使用bash(2810)运行了20150814pstree.sh(3263)这个进程。
20150814pstree.sh(3263)这个进程又运行了20150814pstree_sleep.sh(3622)这个进程,grep进程,pstree进程
Step2:在原有基础上再开一个窗口(相当于一个bash文件),再次运行20150814pstree.sh程序,
可以看出,多出了bash(3739)这个进程,对应新打开的窗口,之后的20150814pstree.sh(3976)进程,以及其子进程同上面的解释
Step3:此时,将先前的窗口关闭,在新窗口中重新运行20150814pstree.sh程序,
1、bash(3739)的进程号不变
2、20150814pstree.sh的进程号变化了(因为是新的)
3、之前的窗口被关闭了,所以对应的进程消失了,然而它的子进程也消失了
Step4:
此时,键入 ps -l 命令
来分析一下,
bash(3739)->20150814pstree.sh(4533)->sleep(5850)
->20150814pstree_sleep.sh(4534)->sleep(5848)
->ps(5987)
init(1)->20150814pstree_sleep.sh(3918)->sleep(5987)
->20150814pstree_sleep.sh(3977)->sleep(5986)
在前一个窗口中,使用CRTL+C关闭了20150814pstree.sh,而对应的后台运行的子进程20150814pstree_sleep.sh都没有管,成为了“僵尸”进程,就归init管了。
这时候,可以使用kill 指令将其关闭。
本文中代码可以从github上面下载,https://github.com/yifeng152/oneShellPracticePerDay.git
对应文件为20150814pstree.sh