说进程之前要先说一下程序,相信大家都知道什么是程序,程序就是一个可执行文件,是一堆指令的集合。相对而言程序是静态的。而运行起来的程序就是进程,是动态的,是程序执行的过程。程序可以运行多次比如QQ可以启动多个,但是每一个都会在内存中有独立的隔离空间用于装载程序代码和数据。
我们通过ps命令可以查看Linux系统中当前运行的进程
1
|
ps
-aux
|
具体字段请看 Linux系统的监控命令
这里面每一行都是一个进程,PID是进程号,后面的COMMAND这是具体进程或者说是程序名称。第一行永远都是PID 1的系统init进程,这个是开机内核创建的,它会一直运行直到关机,在它产生的初期是在内核态运行,然后通过系统调用,执行/sbin/init程序,从内核态切换到用户态。以后所有的用户进程都是由这个进程派生出来,它是所有用户进程的父进程,也就是说Linux内核并不直接建立用户进程。
那用户运行程序是怎么变成进程的呢?其实都是init进程通过调用fork函数复制一个自己,然后去exec最终要执行的程序。
1
|
top
#默认按照进程来查看
|
1
|
top
-H
#按照线程来查看
|
1
|
top
-H -p PID
#查看进程中跑了多少线程,其实也就是这个进程派生出来的线程
|
关于fork:
fork的作用就是复制一个与自己一样的进程,新进程的变量、参数等都和原来的一样,但是它是一个全新的进程,并且作为原来进程的子进程。
可是通常情况下复制了自己以后就马上执行exec函数集合,调用可执行程序来取代调用该执行程序的进程内容,这就会导致原来复制的数据都白费了。所以在fork复制的时候采用了写时复制技术,也就是新的子进程和父进程都有独立的虚拟内存地址,不过这个虚拟地址都指向相同的物理地址,这就保证了它们有独立的逻辑空间,如果子进程需要修改数据的时候,在为其分配独立的物理空间,然后在把数据复制过去。但是要注意它并不是所有的东西都是用到才复制,比如虚拟地址空间结构(mm结构)、父进程的页表信息都是实实在在复制过去的,因为任何进程都是task_struct结构的实例。
所以fork就是复制一个和自己一样的进程并作为自己的子进程存在,然后由子进程去调用真正需要执行的程序。这就要用到exec函数集合。
还有一个叫做vfork,只要是跟内存有关的东西都不复制了,父子进程内存完全共享。为了避免共同操作同一个栈,当子进程生成以后,父进程就被挂起,直到子进程调用了exec函数并有了自己独立的空间或者子进程退出。如果使用vfork然后马上执行exec的话效率会比用fork要高。和fork相比,vfork节省的最大开销就是对页表的拷贝。
说明:进程都是task_struct这个数据结构的实例,这个也被称为进程描述符它记录了进程的上下文,其中有一个叫做内存描述符的数据结构(mm_struct)它描述了进程地址空间的所有信息,它包括代码段、数据段等。每个进程都有自己独立的mm_struct。即使是fork一个进程,子进程也有独立的task_struct并且有独立的mm_struct,只是它的虚拟地址空间映射到了父进程的物理地址空间而已,如果是vfork则完全不用,父子使用相同的虚拟地址空间,当然也就共享同样的物理地址空间。
fork()函数的返回值说明:
返回值 |
说明 |
PID值 |
说明当前在父进程中 |
0 |
说明当前在子进程中 |
负值 |
出现错误 |
关于exec函数集合:
exec函数集合的作用就是根据指定的文件名找到可执行文件(使用系统环境变量或者接收一个传递过来的环境变量),并用这个可执行文件取代调用进程(调用进程就是调用该exec函数集的进程,理解为上面提到的子进程)的内容。
exec函数执行成功后不会返回信息,因为调用进程(子进程)的实体包括代码、数据和堆栈都是存在的,只是被新的可执行程序所取代,只有进程ID还是和原来一样(子进程和父进程的ID可是不同的,fork一个进程就会为其分配一个独立的ID号),你可以理解为把一个程序装入到子进程中,所以不能算是全新的创建;但是如果执行失败会返回-1.
exec函数集合,如下:
arg为列表参数
int execl(const char *path, const char *arg, ...); path是可执行程序的完整路径
int execlp(const char *file, const char *arg, ...); file是可执行程序的文件名,无论是path还是file都会自动使用系统当前环境变量
int execle(const char *path, const char *arg, ..., char * const envp[]); envp是接收自定义的环境变量来找可执行文件
arg为数组参数
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execve(const char *path, char *const argv[], char *const envp[]); envp是接收自定义的环境变量来找可执行文件
上面只有execve是真正会被执行的,其他都是经过了不同封装因为传递的参数会有不同,但是他们最终也是要执行execve的。
再说回进程,当子进程退出时,会通知父进程,让父进程来清理自己的内存空间,并在内核里留下自己的退出信息,如果退出正常完成该code就是0,如果不正常就是大于0的整数。父进程调用wait函数清理子进程使用的内存空间,并且可以获取到子进程的退出信息。如果子进程没有退出,而父进程退出了呢?那么子进程就会被init进程所接管,成为子进程的新父进程,而之前退出的父进程是init进程的子进程,所以init就会调用wait函数去清理其使用的内存然后获取退出信息。
从上图我们可以看到进程的树形结构init在最顶层。
Linux中到底有没有线程概念呢?如果你理解的线程是Windows平台下的那种线程的话,那在Linux中则没有。确切的说Linux有的就是进程,而没有真正的线程,它的线程其实就是一通特殊的进程而已。fork出来的进程你也可以说它是线程,不过跟父进程比,它是更加轻量级的。那Linux中的线程库是干嘛的呢?那个只是用来模拟线程操作而已。
本文转自linuxjavachen 51CTO博客,原文链接:http://blog.51cto.com/littledevil/1868967,如需转载请自行联系原作者