Linux多进程

0

本文涉及:

  • Linux多进程
  • fork函数
  • 控制子进程执行流程
  • 僵尸进程
  • 孤儿进程

Linux的0、1、2号进程

  • idle进程:系统创建的第一个进程,加载系统
  • systemd进程:系统初始化,是所有其他用户进程的祖先,早期为init进程
  • kthteadd进程:负责所有内核线程的调度和管理

Linux不提供创建进程的调用,故父进程创建子进程都是克隆自己产生新进程,除了系统引导时内核创建的进程外所有的进程都必须由另一个进程创建。可以在系统中挑一个进程,通过不断追溯祖先进程,可以发现,所有的进程的祖先进程都是1号或2号进程。

进程标识

  • 每个进程都有一个非负整数表示的唯一进程ID
  • 查看进程:ps -ef | grep 进程名
  • getpid(void)获取进程ID
  • getppid(void)获取父进程ID

查看进程ID示例:

1
2
3
4
5
6
7
int main()
{
cout << "getpid()= " << getpid() << endl;
cout << "getppid()= " << getppid() << endl;
sleep(60); //不让太快结束
return 0;
}

fork函数

fork函数,是UNIX或类UNIX中的分叉函数,fork函数将运行着的程序分成2个(几乎)完全一样的进程,每个进程都启动一个从代码的同一位置开始执行的线程。这两个进程中的线程继续执行,就像是两个用户同时启动了该应用程序的两个副本。

  • 一个现有的进程调度函数fork创建一个新的进程,子进程
  • 子进程和父进程继续执行fork函数后面的代码
  • fork函数调用一次返回两次
  • 子进程返回0,父进程返回子进程的进程ID
  • 子进程是父进程的副本
  • 子进程获得了父进程的数据空间、堆和栈的副本,不是共享
  • 父进程中打开的文件描述符也被复制到子进程中
  • 如果父进程先退出,子进程会成为孤儿进程,将会被1号进程收养,由1号进程完成对它们状态收集的工作
  • 如果子进程先退出,内核向父进程发送SIGCHLD信号,如果父进程不处理这个信号,子进程会成为僵尸进程

使用fork函数创建子进程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int main()
{
cout << "aaa=" << getpid() << endl;
sleep(10); //方便查看进程
cout << "bbb=" << getpid() << endl;

int id = fork(); //创建子进程
cout << id << endl;

sleep(1);
cout << "ccc=" << getpid() << endl;
sleep(30);
cout << "ddd=" << getpid() << endl;

return 0;
}

运行结果

test运行结果 test进程ID
1 2

父进程24805创建子进程24868,二者分别继续执行fork函数之后的语句,父进程调用fork函数返回了子进程ID24868,子进程调用fork函数返回0。

实现子进程流程控制

使用fork函数创建子进程,当然是希望新的进程处理不同的功能。子进程返回0,父进程返回子进程的进程ID,利用fork函数的返回值,可以编程实现控制。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int main()
{//父进程和子进程执行不同
int pid = fork();

if(pid==0){
cout << "这是子进程" << getpid() << ",将执行子进程任务" << endl;
sleep(20);//模拟执行
}
if(pid > 0){
cout << "这是父进程" << getpid() << ",将执行父进程任务" << endl;
sleep(20);//模拟执行
}

return 0;
}

运行结果

test运行结果 test进程ID
3 4

子进程获得了父进程空间的副本

子进程获得的只是父进程内存空间的副本,要区分线程共享进程的空间,这里只是复制一份,二者不共享空间。

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
int main()
{//子进程获得了父进程的数据空间、堆和栈的副本,不是共享
int pid = fork();
int ii = 1;
if(pid==0){
//cout << "这是子进程" << getpid() << ",将执行子进程任务" << endl;
cout << "aaa ii=" << ii++ << endl;sleep(1);
cout << "aaa ii=" << ii++ << endl;sleep(1);
cout << "aaa ii=" << ii++ << endl;sleep(1);
cout << "aaa ii=" << ii++ << endl;sleep(1);
cout << "aaa ii=" << ii++ << endl;sleep(1);
//sleep(20);//模拟执行
}
if(pid > 0){
//cout << "这是父进程" << getpid() << ",将执行父进程任务" << endl;
cout << "bbb ii=" << ii << endl;sleep(1);
cout << "bbb ii=" << ii << endl;sleep(1);
cout << "bbb ii=" << ii << endl;sleep(1);
cout << "bbb ii=" << ii << endl;sleep(1);
cout << "bbb ii=" << ii << endl;sleep(1);
//sleep(20);//模拟执行
}

return 0;
}

运行结果

5

在上述的例子中,子进程对变量ii进行自增运行,但是父进程打印出的ii值并没有改变。说明两个进程使用的空间互不干涉。

父进程中打开的文件描述符被复制到子进程中

父进程中打开的文件,子进程同样可以修改。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int main()
{//父进程中打开的文件描述符也被复制到子进程中
ofstream output("/tmp/tmp.txt",ofstream::out | ofstream::app);//以写方式打开文件,每次写定位在末尾
output << "这是一条数据" << endl;
int pid = fork();
if(pid==0){
cout << "这是子进程" << getpid() << ",将执行子进程任务" << endl;
output << "aaa 这是一条数据" << endl;
//sleep(20);//模拟执行
}
if(pid > 0){
cout << "这是父进程" << getpid() << ",将执行父进程任务" << endl;
output << "bbb 这是一条数据" << endl;
//sleep(20);//模拟执行
}
output.close();
return 0;
}

6

父进程和子进程是两个独立的进程,如果在父进程关闭文件,不会影响子进程,同理,子进程关闭文件,亦不会影响父进程。

僵尸进程

僵尸进程是当子进程比父进程先结束,而父进程又没有回收子进程,释放子进程占用的资源,此时子进程将成为一个僵尸进程。如果父进程先退出 ,子进程被systemd接管,子进程退出后1号进程会回收其占用的相关资源。

1
2
3
4
5
6
7
8
9
10
11
int main()
{//演示僵尸进程
int pid = fork();
if(pid==0){
sleep(5);//模拟执行
}
if(pid > 0){
sleep(10);//模拟执行
}
return 0;
}

运行结果:

7

图中黄色标记处,此时ID为14111的子进程运行已经结束,但是父进程运行并没有结束,成为僵尸进程。

如果子进程在父进程之前终结,内核为每个子进程保留一个数据结构(进程控制块pcb),包括进程编号、终止状态和CPU使用时间等,如果父进程没有处理子进程退出的信息,内核就不会释放这个数据结构,子进程就一直占着进程编号。系统可用的进程控制块是有限的,如果大量僵尸进程,新进程将因为分配不到pcb,系统也就无法创建新的进程。

解决僵尸进程的方法:

  • 在父进程中忽略SIGCHLD信号signal(SIGCHLD,SIG_IGN);
  • 在父进程中增加等待子线程退出的代码wait(&sts);
  • 使用信号处理函数

在父进程中忽略SIGCHLD信号

1
2
3
4
5
6
7
8
9
10
11
12
int main()
{
signal(SIGCHLD,SIG_IGN);//忽略子进程结束信号
int pid = fork();
if(pid==0){
sleep(5);//模拟执行
}
if(pid > 0){
sleep(10);//模拟执行
}
return 0;
}

使用signal(int sig, void (*func)(int))函数时,需要包含<csignal>头文件,想要知道应该引入哪个头文件,可以使用如下命令:

1
man 3 signal

man 2 是获得系统(linux内核)调用的用法 。
man 3 是获得标准库(标准C语言库、glibc)函数的文档。

执行结果:

运行5秒内 运行5秒后
8 9

子进程运行结束后,父进程忽略子进程结束信号,子进程被系统回收。

在父进程中增加等待子线程退出的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
int main(){
//signal(SIGCHLD,SIG_IGN);//忽略子进程结束信号
int pid = fork();
if(pid==0){
sleep(5);//模拟执行
}
if(pid > 0){
int sts;
wait(&sts);
sleep(10);//模拟执行
}
return 0;
}

运行结果与上面忽略信号类的方法一样,但是wait会阻塞父进程,使得父进程无法推进。要解决这个问题,可以使用第三种方法。

使用信号处理函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void func(int sig){//定义信号处理函数
int sts;
wait(&sts);
}
int main(){
signal(SIGCHLD,func);//忽略子进程结束信号,5秒后子进程向父进程发软中断信号,父进程调用func函数处理中断
int pid = fork();
if(pid==0){
sleep(5);//模拟执行
}
if(pid > 0){
sleep(10);//模拟执行
}
return 0;
}

运行结果:

运行5秒内 运行5秒后
10 11

但是,上述解决方法存在一个问题,5秒时父进程收到子进程的软中断,暂停执行sleep(),转去执行func(),处理完中断后,父进程结束。所以说,收到软中断信号时,父进程正在执行的语句会被打断。

孤儿进程

如果父进程先退出,子进程会成为孤儿进程,将会被1号进程收养,由1号进程完成对它们状态收集的工作。不像是僵尸进程,孤儿进程对系统没有危害。

1
2
3
4
5
6
7
8
9
10
11
int main()
{//演示孤儿进程
int pid = fork();
if(pid==0){
sleep(10);//模拟执行
}
if(pid > 0){
sleep(5);//模拟执行
}
return 0;
}

运行结果:

运行5秒内 运行5秒后
12 13

运行5秒后,主程序退出运行,此时查看进程快照,子进程被1号进程收养,继续执行直到结束。

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