0%

forkthreadcreate

前言

这篇文章,主要是想讲明白所有fork函数和threadcreate函数带来的数进程线程问题,变量值是多少的问题。操作系统中有很多这种题,每次做的时候,看着一大串源码,我总是想,发生甚么事了❓所以,借此机会,写一篇文章,看能不能把这种类型的题彻底讲明白。


fork()函数

什么是fork()

首先,让我们想想,如果我们没有学过操作系统,我们第一眼看到fork,会想到什么?

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
#include <unistd.h>  
#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
int main ()
{
pid_t fpid; //fpid用来储存fork函数返回的值
int count = 0;
fpid = fork();
if (fpid < 0)
printf("出现错误!");
else if (fpid == 0) {
printf("子进程,我的进程号是%d\n我的父进程的进程号是%d\n",getpid(),getppid());
count++;
}
else {
wait(NULL);
printf("父进程,我的进程号是%d\n我的父进程的进程号是%d\n",getpid(),getppid());
count++;
}
printf("统计结果是: %d\n",count);
printf("------\n");
return 0;
}

运行结果为

1
2
3
4
5
6
子进程,我的进程号是5574,我的父进程的进程号是5573
统计结果是: 1
------
父进程,我的进程号是5573,我的父进程的进程号是5562
统计结果是: 1
------

让我们从上往下看代码,首先使用了fork函数,创建了一个子进程,然后出现了一个fpid,用来存储fork()函数的返回值,但是在下面,就有点奇怪。

  • 为什么可以判断一个函数的返回值,然后做出不同的操作呢?

这是因为,fork函数非常的特殊,在虽然是一个函数,但是有两个返回值,fork函数就是通过不同的返回值来区分父进程和子进程,父类的fpid返回的是子类的进程号,子类的fpid中返回的是0,这么做的目的是区分父子函数,前面我们提到,fork函数创建的子进程只有少数值与父类不同,在其他方面相当于克隆了一个父进程,这时,就要通过fork函数的不同返回值来区分父子进程

  • 什么是getpid()函数,什么是getppid()函数?

getpid()函数的作用是返回当前进程的进程号(pid),我们需要注意,在子进程中,虽然用来储存fork()函数返回值的fpid的值为0,但是并不代表子进程的进程号为0

getppid()函数的作用是返回当前进程的父进程号(),这里特殊的是,我们所谓的父进程其实也是别的进程的子进程,就像我们的爹,同时也是我们👴的儿子,所有进程的祖先是init进程,在新版的Linux系统中,init已经改为systemd,以提高启动速度,也就是说,现在在Linux系统中搜索pid = 1的进程,会得到systemd

  • 为什么count的值在经历过两次自加之后,值还是为1?

其实要解决这个问题,只需要仔细看前面**什么是fork()**的部分

系统先给新的进程分配资源,例如存储数据和代码的空间。然后把原来的进程的所有值都复制到新的进程中

类似于复制粘贴一份文件,在修改新文件时,并不会对旧文件中的内容产生影响,在这里,父进程和子进程虽然都有一个count,但是对count值的操作只会影响到本进程的count,而不会影响到另一个进程。


代码二数数问题

1
2
3
4
5
6
7
8
9
10
#include <stdio.h> 
#include <sys/types.h>
int main()
{
fork();
fork();
fork();
printf("hello\n");
return 0;
}

或者是使用for循环

1
2
3
4
5
6
7
8
9
10
#include <stdio.h> 
#include <sys/types.h>
int main()
{
for (int j = 0; j < 3; j++) {
fork();
}
printf("hello\n");
return 0;
}

结果

1
2
3
4
5
6
7
8
hello 
hello
hello
hello
hello
hello
hello
hello

这个就是一个基本的数数问题,在我第一次做这种题的时候,我会去画树状图,但是如果从数学的角度去考虑,所有的可以得到这样一个公式

我们知道,只要经历一次fork函数,总进程数就会翻倍一次,那么经历n次之后,就能得到上面的式子,在上面的代码中,用这个公式就能轻易的算出总进程数是等于8的

  • 在第三步创建了多少新进程?

这也很简单嘛,只需要用第三步的总进程数减去第二步的总进程数,就能得到第三步新创建的进程数


wait()函数

什么是wait()

wait()会暂时停止目前进程的执行,直到有信号来到或子进程结束。如果在调用wait()时子进程已经结束,则wait()会立即返回子进程结束状态值。

简单来说,wait函数就是等待的意思,如果在父进程中使用wait()函数,则父进程会等待子进程结束后再继续执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <unistd.h>  
#include <stdio.h>
int main ()
{
pid_t fpid; //fpid用来储存fork函数返回的值
int count = 0;
fpid = fork();
if (fpid < 0)
printf("出现错误!");
else if (fpid == 0) {
sleep(2);
printf("子进程,我的进程号是%d\n,我的父进程的进程号是%d\n",getpid(),getppid());
count++;
}
else {
printf("父进程,我的进程号是%d\n,我的父进程的进程号是%d\n",getpid(),getppid());
count++;
}
printf("统计结果是: %d\n",count);
printf("------\n");
return 0;
}

我们不妨把代码一稍稍改一下,去掉父进程中的wait()函数,在子进程中加入sleep()函数,让我们来看看执行结果有什么不同

1
2
3
4
5
6
父进程,我的进程号是7309,我的父进程的进程号是7300
统计结果是: 1
------
子进程,我的进程号是7311,我的父进程的进程号是1770
统计结果是: 1
------

我们发现,跟上面的执行结果有两处不同

  • 第一处是执行的顺序不同了,这次是先输出父进程再输出子进程,而之前是先输出子进程再输出父进程
  • 第二处是子进程的父进程号变了,getppid()函数并没有返回父进程的进程号,而是返回了一个奇怪的进程号1770

第一处问题很好解释,在原版的代码一中,父进程中有wait()函数,是父进程等待子进程执行完,而在改版中的代码一中,由于删去了父进程中的wait()函数,反而在子进程中加入了sleep()函数,这会导致父进程先于子进程执行完,所以先输出父进程的结果,再输出子进程的结果

第二处问题就比较有意思了,我们知道,父进程再执行完之后就会退出,这时子进程还没执行完,子进程就会变成我们所说的孤儿进程

我们知道,孤儿进程的父进程会变成1,代表的是init进程,但是在前面我们提到过,新版的Linux系统采用更加复杂的systemd代替了init,所以在这里情况会有所不同。

让我们来查询一下进程号为1770的进程是什么,在ubuntu终端中输入以下命令

1
ps -e | grep 1770

最终可以得到结果

1
1770  ?  00:00:00  systemd

我们可以知道,虽然新版的Linux系统中不再将孤儿进程的父进程设定为1,但是仍然将孤儿进程的父进程设置为系统守护进程


pthread_create()函数

1
pthread_create(pthread_t*restrict tidp,const pthread_attr_t *restrict_attr,void*(*start_rtn)(void*),void *restrict arg);

我们可以看到,在pthread_create()函数中,一共需要传入四个参数

  • 第一个参数为指向线程标识符的指针。
  • 第二个参数用来设置线程属性。
  • 第三个参数是线程运行函数的起始地址。
  • 第四个参数是运行函数的参数。

其实在这四个当中,我们只需要记住第三个参数用来表示线程运行函数时的起始地址就足够了


代码一流程问题

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 <pthread.h> 
#include <stdio.h>
#include <types.h>

int value = 0;
void *runner(void *param); /* the thread */

int main(int argc, char *argv[])
{
pid_t pid;
pthread_t tid;
pthread_attr_t attr;

pid = fork();
if (pid == 0) { /* child process */
pthread_attr_init(&attr);
pthread_create(&tid,&attr,runner,NULL);
pthread_join(tid,NULL);
printf("CHILD: value = %d",value); /* LINE C */
}
else if (pid > 0) { /* parent process */
wait(NULL);
printf("PARENT: value = %d",value); /* LINE P */
}
}

void *runner(void *param) {
value = 5;
pthread exit(0);
}

这是我们的一道作业题,问题是第C行和第P行的输出值是多少?

其实,这个题最后也没有问任何与线程相关的东西,如果把这题中有关线程的东西去掉,这段代码就会变得通俗易懂

在这道题当中,用了大段的代码来创建一个进程去执行runner函数中的内容,其中比较关键的地方有

1
2
pthread_create(&tid,&attr,runner,NULl);
pthread_join(tid,NULL);

第一行代码指的是,我们可以把目光直接移动到runner函数,至于这个函数是用哪个线程干的,并不影响我们做题

如果仅仅从做题的角度,我们可以把这行代码等同于机组中的jal(误)

1
jal label(label是pthread_create函数中的第三个参数)

那让我们继续看看pthread_join()函数是来干什么的

  • pthread_join()等同于wait()函数,指的是以阻塞的方式等待某一个线程执行结束(tid指向的线程)

如果看懂了这两行关键的代码,从做题家的角度,上面冗长的代码就可以转化为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>
#include <types.h>
int value = 0;
int main(int argc, char *argv[])
{
pid = fork();
if (pid == 0) { /* child process */
value = 5;
printf("CHILD: value = %d",value); /* LINE C */
}
else if (pid > 0) { /* parent process */
wait(NULL);
printf("PARENT: value = %d",value); /* LINE P */
}
}

好家伙,代码直接从30行变成15行


代码二数数问题

1
2
3
4
5
6
7
8
pid_t pid;

pid = fork();
if (pid == 0) { / * child process * /
fork();
thread_create( . . .);
}
fork();

这个也是我们的一道作业题,问题是

  • 执行一遍之后,有多少个进程被创建?又有多少个线程被创建?

在做这个题之前,我们首先要弄明白一个问题


线程与进程的关系

这里参考了这篇博客,我来简单转述下

  • 我们不妨来做一个简单的比方
  • CPU是一个工厂
  • 进程是一个车间
  • 线程是车间的工人

  • 一个工厂时刻在运行,但是由于电力原因,一次只能开一个车间

    一个CPU一次只能执行一个进程

  • 一个车间里可以有很多工人一起协同工作

    不同的任务可以分配给不同的线程去做

  • 一个车间中的生产区域,每一个工人都能进去

    一个进程的内存空间是共享的,每一线程都可以进去

  • 车间中的有些房间一次只能容纳一个人,比如厕所

    一个线程使用某些共享内存时,其他线程必须等它结束,才能使用这一块内存。

  • 一个工人进厕所之后会锁门,防止其他的工人进去

    存在互斥锁,防止多个线程同时读写某一块内存区域

  • 一个正在生产中的工厂,至少有一个工人在某一个车间中进行生产

    一个程序至少有一个进程,一个进程至少有一个线程。


让我们来接着看数数问题

1
2
3
4
5
6
7
8
pid_t pid;

pid = fork();
if (pid == 0) { / * child process * /
fork();
thread_create( . . .);
}
fork();

这里我想换一个思路,讲讲在stack overflow中,这个题的三个主流解法

第一种—有6个进程,2个线程被创建

-------------本文结束感谢您的阅读-------------