目录

进程控制

什么是进程

进程:是程序的一次执行,是系统进行资源分配和调度的基本单位。

程序与进程

  • 进程是正在运行的程序的实例

  • 进程是动态的、永存的;程序是静态的、暂时的

  • 1 个程序可以对应多个进程,但 1 个进程只能对应 1 个程序

  • 进程存在并发性和独立性,没有建立 PCB 的程序不能参与并发执行和独立执行。

进程创建

https://s1.ax1x.com/2020/10/26/BnLab6.png
进程创建

Linux (UNIX) 中使用 fork() 函数创建一个子进程,子进程与父进程同时执行。

由于在子进程创建时复制了父进程的堆栈段,所以两个进程都停留在 fork 函数中等待返回,

所以当 fork 函数在父进程被调用一次,会返回两次(父子进程各一次),返回值有三种情况:

  • 在父进程中返回子进程的 PID

  • 在子进程中返回 0

  • 创建失败返回 -1

例1:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
#include <stdio.h>
#include <unistd.h>
int main()
{
    int pid;
    while ((pid = fork()) == -1)
        ;
    if (pid == 0)
        putchar('b'); // 子进程
    else
        putchar('a'); // 父进程
}

输出结果 abba 随机出现。

Q: 为什么会有两个结果?

A: 调用 fork 函数后会产生一个新进程,此时程序中有父子两个相互独立的进程,程序执行一次,两个进程各执行一次,产生了两个结果。

Q: if - else 起什么作用?

A: if - else 只是区分父子进程的执行内容,fork 后父子进程分别进入判断,执行对应内容。

Q: 父子进程的执行顺序如何?

A: 执行程序时,父进程首先启动,执行到 pid = fork() 会创建一个子进程,此时父子进程并发执行,父子进程的执行顺序由内核的进程调度算法决定,具有随机性。

例2:在系统中有一个父进程和两个子进程活动。让每个进程在屏幕上显示一个字符;父进程显示字符 a,子进程分别显示字符 bc

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
#include <stdio.h>
#include <unistd.h>
int main()
{
    int p1, p2;
    while ((p1 = fork()) == -1)
        ;
    if (p1 == 0)
    {
        putchar('b'); // 子进程
    }
    else
    {
        while ((p2 = fork()) == -1)
            ;
        if (p2 == 0)
            putchar('c'); // 子进程
        else
            putchar('a'); // 父进程
    }
}

结果为 bacbcaacbabc

该程序先后创建了两个进程,最终有三个进程并发执行,输出结果有三个。

进入 if 判断,如果满足 p1 = 0,则是子进程 1,子进程 1执行输出 b,如果满足 p1 > 0为父进程部分,并创建子程序 2。

如果满足 p2 = 0,则是子程序 2,子程序2执行输出 c,如果 p2 > 0 则是父进程,父进程执行输出 a

综上,父进程 a 创建了两个子进程 bc(通过输出各自的父子进程 PID,可知 bc 的父进程 PID 都为 aPID)。

由于创建进程所需的时间可能多于输出字符的时间,在创建第二个进程之前,子进程 1可能已经执行完毕,所以 b 首先输出(不一定,父进程和子进程 1 输出顺序有随机性)。以上父子进程的执行顺序由内核的进程调度算法决定,也可理解为父子进程抢夺系统资源,所以并发执行中 abc 的输出先后顺序具有随机性。

进程控制

例3:在例 2 的基础上修改,将每个进程的输出由单个字符改为一句话,再观察程序执行的结果。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <stdio.h>
#include <unistd.h>
int main()
{
    int p1, p2, i;
    while ((p1 = fork()) == -1)
        ;
    if (p1 == 0)
        for (i = 0; i < 500; i++)
            printf("daughter %d\n", i);
    else
    {
        while ((p2 = fork()) == -1)
            ;
        if (p2 == 0)
            for (i = 0; i < 500; i++)
                printf("son %d\n", i);
        else
            for (i = 0; i < 500; i++)
                printf("parent %d\n", i);
    }
}

结果:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
...
daughter  237
parent  106
son  149
daughter  238
son  150
...
parent  497
parent  498
parent  499

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
...
daughter  213
parent  145
daughter  214
son  134
daughter  215
...
son  497
son  498
son  499

Q: 例 3 结果与例 2 有什么异同?

A: 例 2 是单字符输出,例 3 循环输出字符串,但是每次 printf 输出字符串时不会被中断,因此字符串内部字符顺序输出不变。由于并发执行的父子进程调度顺序和抢占资源的问题,执行输出的顺序与例 2 一样存在随机性。

Q: 起始 i 值为什么不是 0?

A: 由于输出行数过多,终端只会输出后面部分,如果循环次数为 10、50 等小数目,终端能从 0 开始完整显示(测试在 vscode 调试环境下,如果在 CLI 的 gcc 编译运行能完整输出)。

若将循环次数改为 5,结果为:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
daughter  0
daughter  1
daughter  2
daughter  3
daughter  4
son  0
parent  0
son  1
parent  1
son  2
parent  2
parent  3
son  3
parent  4
son  4

例4:使用系统调用 lockf() 来给每个程序加锁,观察并分析结果。

 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
#include <stdio.h>
#include <unistd.h>
int main()
{
    int p1, p2, i;
    while ((p1 = fork()) == -1)
        ;
    if (p1 == 0)
    {
        lockf(1, 1, 0); // 加锁
        for (i = 0; i < 500; i++)
            printf("daughter %d\n", i);
        lockf(1, 0, 0); // 解锁
    }
    else
    {
        while ((p2 = fork()) == -1)
            ;
        if (p2 == 0)
        {
            lockf(1, 1, 0); // 加锁
            for (i = 0; i < 500; i++)
                printf("son %d\n", i);
            lockf(1, 0, 0); // 解锁
        }
        else
        {
            lockf(1, 1, 0); // 加锁
            for (i = 0; i < 500; i++)
                printf("parent %d\n", i);
            lockf(1, 0, 0); // 解锁
        }
    }
}

结果:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
daughter 0
daughter 1
daughter 2
...
son 0
son 1
son 2
...
parent 497
parent 498
parent 499

Q: 例 3 结果与例 4 有何异同?

A: 加锁前和加锁后输出的内容基本一致,而且 parentdaughterson 每块的输出顺序同样存在随机性,不过加锁后每块进程的执行不会被打断,即执行完一个进程再去执行另外一个进程。

Q: lockf() 有何作用?用法如何?

A: lockf(files,function,size) ,其中,file 为文件描述符,1 表示标准输出设备,function 是锁定和解锁:1 表示加锁,0 表示解锁; size 是锁定或者解锁的字节数,一般为 0,表示从文件的当前位置到文件尾。给进程加锁后,会将标准输出设备锁住,其他进程无法获取资源输出,在该进程输出语句执行完后,将标准输出设备解锁,其他进程才可输出。这样就可防止父子进程竞争输出资源,实现进程之间的互斥。

Q: 解锁条件有哪些?

A: 调用 lockf 函数解锁,或者等当前加锁的进程结束后自动释放锁。

进程树创建

例5:创建进程树,在每个进程中显示当前进程识别码和其父进程识别码。

1
2
3
4
a               - 父进程
└── b
    └── c       } 子进程
        └── d 
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <stdio.h>
#include <unistd.h>
int main()
{
    int p1, p2, p3;
    while ((p1 = fork()) == -1)
        ;
    if (p1 > 0)
        printf("process a (its id = %d, its father pid = %d)\n", getpid(), getppid());
    else
    {
        printf("precess b (its id = %d, its father pid = %d)\n", getpid(), getppid());
        while ((p2 = fork()) == -1)
            ;
        if (p2 == 0)
        {
            printf("precess c (its id = %d, its father pid = %d)\n", getpid(), getppid());
            while ((p3 = fork()) == -1)
                ;
            if (p3 == 0)
                printf("precess d (its id = %d, its father pid = %d)\n", getpid(), getppid());
        }
    }
}

结果:

1
2
3
4
process a (its id = 3918, its father pid = 3912)
precess b (its id = 3923, its father pid = 3918)
precess c (its id = 3924, its father pid = 3923)
precess d (its id = 3925, its father pid = 3924)

进程树:

1
2
3
4
a               PID = 3918
└── b           PID = 3923
    └── c       PID = 3924
        └── d   PID = 3925

b 的父进程 PID 是 a 的 PID,所以 ab 是父子关系,而 c 的父进程 PID 是 b 的 PID,即 bc 是父子关系,这样就可以构建成一个由 abc 组成的进程树,a 就是 b,c 的父进程。abcd 进程树的创建同理。