进程的概念

  • 进程是一个程序的一次执行的过程,同时也是资源分配的最小单元。
  • 进程和程序是有本质区别的,程序是静态的,它是一些保存在磁盘上的指令的有序集合,没有任何执行的概念;
  • 进程是一个动态的概念,它是程序执行的过程,包括了动态创建、调度和消亡的整个过程。

进程的状态:

  • 执行态:该进程正在运行,即进程正在占用CPU。
  • 就绪态:进程已经具备执行的一切条件,正在等待分配CPU的处理时间片。
  • 等待态:进程不能使用CPU,若等待事件发生(等待的资源分配到)则可将其唤醒。

进程中的内存分布:

  • 代码段:存放的是程序代码的数据。
  • 数据段:存放的是全局变量、常数以及动态数据分配的数据空间,根据存放的数据,数据段又可以分成普通数据段(包括可读可写/只读数据段,存放静态初始化的全局变量或常量)、BSS数据段(存放未初始化的全局变量)以及堆(存放动态分配的数据)。
  • 堆栈段:存放的是子程序的返回地址、子程序的参数以及程序的局部变量等。

进程内存分布

进程的创建

  • fork()函数用于从已存在的进程中创建一个新进程。新进程称为子进程,而原进程称为父进程。
  • 使用fork()函数得到的子进程是父进程的一个复制品,它从父进程处继承了整个进程的地址空间,包括进程上下文、代码段、进程堆栈、内存信息、打开的文件描述符、信号控制设定、进程优先级、进程组号、当前工作目录、根目录、资源限制和控制终端等。
  • 子进程所独有的只有它的进程号、资源使用和计时器等。
  • fork()函数的系统开销比较大,而且执行速度也不是很快。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
pid_t result;

/*调用fork函数,其返回值为result*/
result = fork();

/*通过result的值来判断fork函数的返回情况,首先进行出错处理*/
if(result == -1)
{
printf("Fork error\n");
}
else if (result == 0) /*返回值为0代表子进程*/
{
printf("The return value is %d\nIn child process!!\nMy PID is %d\n",result,getpid());
}
else /*返回值大于0代表父进程*/
{
printf("The return value is %d\nIn father process!!\nMy PID is %d\n",result,getpid());
}

return result;
}

写时拷贝技术(copy_on_write):由于 fork 完整的复制了父进程的整个空间,因此执行速度比较慢。为了加快执行速度,有些unix 系统创建了 vfork 方法。 vfork 也能创建进程,但是不会产生父进程的副本,而是通过内存映射的方式,让子进程和父进程访问相同的物理内存,从而伪装了对父进程空间的拷贝。当子进程真正要改变内存中的数据时,才会真正的复制父进程的内存空间。

进程的回收

僵尸进程

  • 进程执行结束后,如果不做任何处理,进程不会消失,而是留下一个僵尸进程。
  • 几乎已经放弃了所有的内存空间,无可执行代码,不用被调度。但是会在进程列表中保存一个记录,记录该进程退出状态等信息。
  • 僵尸进程如果不及时回收,也会积少成多,占用内存资源。因此子进程结束后需要由父进程进行回收。
  • 子进程回收方式:在父进程中执行 wait/waitpid 函数

注意:kill -9 的方式杀掉进程,不会有僵尸进程。

exec 函数族

exec 函数可以执行任何的程序。同时,进程当前的内存空间,被 exec 指定的程序替换。
也就是说在程序中 exec 函数调用后,接下来的代码其实是无法执行的。

在 shell 中开启程序,用 ps 命令看到该进程的父进程就是 shell,这是如何实现的?
答案是 shell 利用 fork 函数,创建子进程,用调用 exec 执行shell命令行中的程序。

回想之前 fork 创建子进程后,其执行逻辑和父进程还是同一个程序,只是开发人员通过 pid 判断的方式,做了人为的分割。exec 方法的作用是让父子进程执行不同的程序

守护进程

  • 守护进程也称为后台服务进程,它是一个生存期较长的进程,通常独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。Linux大多数系统服务都是通过守护进程实现的。

由于在Linux中,每一个系统与用户进行交流的界面称为终端,每一个从此终端开始运行的进程都会依附于这个终端,这个终端就称为这些进程的控制终端,当控制终端被关闭时,相应的进程都会自动关闭。但是守护进程却能够突破这种限制,它从被执行开始运转,直到整个系统关闭时才会退出。如果想让某个进程不因为用户、终端或者其他的变化而受到影响,那么就必须把这个进程变成一个守护进程。

进程组

  • 进程组是多个进程的集合。每个进程组都有一个组长,一般组长是父进程,由该进程 fork 出的子孙进程,都属于该进程组。
  • 进程组号:组长进程 pid,且不会因为组长进程退出,改变进程组号。

会话组

  • 会话组是一个进程组或多个进程组的集合。
  • 始于用户登录,终止与用户退出。此期间所有进程都属于该会话组。
  • setsid 作用:让当前进程摆脱原有会话组,进程组,控制终端的控制。

守护进程编写步骤:

  1. fork 进程,父进程关闭。此时子进程被pid 为 1 的 init 进程托管。
  2. setsid,设置单独的 session 组
  3. chdir(“/“) 按照程序自身要求,设置单独的 dir,这样子进程在获取 pwd 的时候,即为设置的路径。【非必须】
  4. umask(0) 按照程序自身要求,设置掩码【非必须】
  5. 关闭之前已打开的文件,否则依然会消耗系统资源,也可能导致文件系统无法卸载。至少要关闭系统为每个程序默认打开的 stdin(0),stdout(1),stderr(2),因为后台无法使用到。写日志可使用 syslog。

umask函数设置掩码:权限掩码由三位数字表示,每位数字对应于对应权限的掩码值。其中,第一位数字表示所有者(owner)的权限掩码值,第二位数字表示同组用户(group)的权限掩码值,第三位数字表示其他用户(others)的权限掩码值。每个数字的取值范围为0到7,其中0表示完全授权,7表示完全禁止。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <syslog.h>

int main(void)
{
pid_t child;
int i;

/*创建子进程1*/
child = fork();

if (child == 1)
{
perror("child1 fork");
exit(1);
}
else if (child1 > 0)
{
exit(0);
}

/*打开日志服务*/
openlog("daemon_proc_info", LOG_PID, LOG_DAEMON);

/*以下几步是编写守护进程的常规步骤*/
setsid();
chdir("/");
umask(0);
for(i = 0; i < getdtablesize(); i++)
{
close(i);
}
}