关于进程的那些事
最近学习了Linux-C应用开发中的进程。这篇文章记录下我个人对进程相关的一些概念的理解和总结。
·进程和程序的对应关系
我们平常写的一些程序编译后得到的可执行文件是存储在磁盘上的,是静态的。而当我们运行这个可执行文件后,它会被系统拷贝到内存中运行,而程序在内存中所对应的便是进程,进程是动态的。
一个程序可以对应一个或多个进程,就是我们将程序多次运行便会得到多个进程,就像电脑上多开QQ一样。而一个进程只能对应一个程序,不可能出现一个进程在执行两个程序的代码。
·进程的开始与结束
对于C语言程序而言,进程总是从main()函数开始执行,而进程的终止通常有以下几种:
①main()函数中通过return语句返回。
②应用程序中调用exit()、_exit()或者_Exit()函数终止进程(正常终止)。
③应用程序中调用abort()函数终止进程(异常终止)。
④进程收到一个会被终止的信号,如SIGKILL。
而进程在调用exit()函数正常终止时可以执行一个用户自定义的回调函数,通过 atexit()函数可以设置。
·进程的环境变量
在我们创建一个进程时,该进程的环境变量会从其父进程中继承而来。环境变量通常用于shell程序中,比如HOME表示当前用户的家目录。
Linux为应用程序提供了一个全局指针变量来指向环境变量,但我们通常不会直接解引用该指针来获取添加或修改环境变量。而是通过Linux提供的函数来操作。
·进程的内存布局
进程的内存布局包含正文段(.text)、数据段(.data)、未初始化数据段(.bss)、栈、堆。正文段保存的是CPU执行的机器语言指令。数据段保存的是已初始化的全局变量和静态变量。未初始化数据段保存的是未初始化的全局变量和静态变量的位置和长度。栈则是保存函数内部的局部变量和调用函数时保存的信息。堆则是可以在运行时进行动态内存分配的区域。
·子进程的创建
在Linux系统下,通常使用fork()函数创建一个新的进程。创建出来的进程是原进程的子进程。子进程会拷贝父进程除了正文段外的所有资源,正文段由于是只读的,因此只需要一份即可。
·子进程与父进程的文件共享
子进程被创建后,由于拷贝了父进程的资源,因此子进程中可以使用父进程在创建子进程前所打开的文件描述符,两个文件描述符均指向同一个文件表,也就是说父子进程操作的是同一份文件内存副本。
·父进程对子进程的监视
由于不确定子进程何时结束,因此通常父进程会阻塞等待子进程结束后才结束。Linux系统提供了一系列wait函数用于父进程对子进程的监视。常用的有wait()、waitpid()、waitid()函数。wait函数除了可以监视子进程外,还用于在子进程终止时回收子进程的资源。
·孤儿进程与僵尸进程
当一个进程终止后,若其还有子进程在运行,则这些子进程变成为孤儿进程,他们将由init进程收养,即它们的父进程变为init进程。
当一个进程终止后,若其父进程还没有调用wait函数回收其资源,则这个进程就变成僵尸进程,僵尸进程无法被杀死。僵尸进程会占用系统进程号PID,过多的僵尸进程会导致系统无法创建新的进程。
避免出现过多僵尸进程的方法通常有2种,在父进程中注册SIGCHLID信号处理函数,在处理函数中回收子进程的资源;而第二种通常是应急处理,就是杀死僵尸进程对应的父进程,让僵尸进程的父进程变为init进程,init进程自然会回收僵尸进程的资源。
·exec族函数
当子进程被创建时,它的内存布局是完全拷贝父进程的,因此执行的代码也与父进程完全相同,但我们创建子进程肯定是需要让它做其他的事情的。exec族函数就是用于将新的程序替换进程原本对应的程序,让子进程执行新的代码。exec族函数中包含7个exec开头的函数,分别是execve()、execl()、execlp()、execle()、execv()、execvp()、execvpe(),其中:
①函数名后带p的函数表示只需要提供程序的文件名,系统会自动在环境变量PATH指定的目录寻找相应的可执行文件。
②函数名后带l函数表示传入程序的main()函数参数argv的方式为:可变长参数。
③函数名后带v的表示传入程序的main()函数参数argv的方式为:argv指针数组。
④函数名后带e的函数表示可以指定自定义的环境变量列表给新程序。
·进程的状态
在Linux下,进程通常存在6种不同的状态:
①就绪态:该进程满足被CPU调度的条件,但还未被调度执行。
②运行态:该进程当前正在被CPU调度运行。
③僵尸态:即僵尸进程,该进程已结束,但父进程还未回收其资源。
④可中断睡眠状态:进程等待某种条件成立,条件成立后会进入就绪态,可以被信号唤醒。
⑤不可中断睡眠状态:同上,但不可被信号唤醒,只能等待相应条件成立。与④统称等待态(阻塞态)。
⑥暂停态:进程暂停运行,通常通过信号暂停,如SIGSTOP。可以恢复到就绪态,通常通过信号恢复,如SIGCONT信号。
·进程的关系
在Linux下,进程关系包括:没有关系、父子关系、进程组和会话。
·父子关系:一个进程由另一个进程创建,则被创建的进程是另一个进程的子进程。
·进程组:一个或多个进程的集合,每个进程组都有一个组长进程,进程组的ID等于组长进程的ID。
·会话:会话是一个或多个进程组的集合,每个会话都有一个会话首领。会话的ID等于会话首领的进程组ID。
·守护进程
守护进程是一种运行在后台的特殊进程,通常生命周期是系统启动到系统关机,且与控制终端脱离关系。守护进程通常用于实现服务器。它们自成进程组、自成会话,其pid == gid == sid。
·单例模式
一个程序可以对应多个进程,就是说一个程序被运行了多次。但有些情况下不允许一个程序被运行多次,只能对应一个进程,这种情况称为单例模式。单例模式通常用在服务器程序,守护进程大部分也是单例模式运行。
令程序以单例模式运行的方法一般是通过文件锁方式,当一个程序第一次运行时,获取到了文件锁,这时再第二次运行该程序,第二个进程无法获取文件锁,表明已经有该程序对应的进程正在运行,此时需要终止第二个进程。

