多进程编程基础

Posted by KalosAner on January 18, 2025

一、引言

本文简单介绍一下多进程编程,Linux 系统提供多进程的系统调用 fork。多进程主要用来并行地执行任务,对于多核 CPU,当 CPU 有空余的核时就会并行地执行其中一个进程,如果没有多余的核时就会分时执行。进程是分配资源的最小单位。每个进程都有一个 PID 和 PPID,其值为大于 2 的整数。PID 为 1 的是 systemd 进程(之前叫做 init 进程),它是所有进程的父进程。可以通过 ps -elf 命令查看所有进程。

二、基础概念

进程的创建通常通过 fork 函数,子进程会共享父进程代码段的内容,复制父进程数据段和 bss 段的内容。

1、进程退出

进程退出分为正常退出和异常退出。

1. 正常退出

进程的正常退出有四种,如下:

1、return 只是代表函数的结束, 返回到函数调用的地方。

2、进程的所有线程都结束。

3、exit() 代表整个进程的结束,无论当前执行到哪一行代码, 只要遇到exit() , 这个进程就会马上结束。

4、 _exit() 或者 _Exit() 是系统调用函数。

_exit() / _Exit 和 exit 的区别:

_exit() / _Exit 是 系统调用函数, exit 是库函数

exit 它是通过调用_exit()来实现退出的

但exit() 多干了两件事情: 清空缓冲区、调用退出处理函数

退出处理函数:

进程正常退出,且调用exit()函数,会自动调用退出处理函数(return 不会调用退出处理函数)

退出处理函数可以做一些清理工作

需要先登记才生效,退出处理函数保存在退出处理函数栈中(先进后出的原则)

退出处理函数可以通过 atexit 函数进行注册

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
 
void func(void)
{
    printf("%s\n",__func__);
}

int main()
{
    atexit(func);//先登记
 
    printf("hello!\n");
 
    exit(0);
    //_exit(0); //无法调用退出处理函数
    //return 0; //无法调用退出处理函数
}
2. 异常退出

1、被信号打断( ctrl + c ,段错误 , kill -9)

2、最后线程(主线程)被取消。

2、进程结束并资源回收

子进程退出时, 不管是正常还是异常,父进程会收到信号

子进程退出后,内存上的资源必须是父进程负责回收

但是有时候会出现下面两种情况 :

1、子进程先结束, 会通知父进程(通过信号), 让父进程回收资源 , 如果父进程不处理信号, 子进程则变成僵尸进程

2、父进程先结束,子进程就会变成孤儿进程, 就会由1号进程(init )负责回收,但在实际编程中要避免这种情况, 因为1号进程很忙

三、fork 函数

函数原型

1
2
3
#include <unistd.h>

pid_t fork(void);

fork 会将一个进程从调用的地方一分为二,子进程和父进程收到的返回值不同用以区分当前进程是子进程还是父进程:

父进程:fork 函数返回子进程 ID

子进程:fork 函数返回 0。

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 gval=10;

int main(int argc, char *argv[])
{
	pid_t pid;
	int lval=20;
	gval++, lval+=5;
	
	pid=fork();		
	if(pid==0)	// if Child Process
		gval+=2, lval+=2;
	else			// if Parent Process
		gval-=2, lval-=2;
	
	if(pid==0)
		printf("Child Proc: [%d, %d] \n", gval, lval);
	else
		printf("Parent Proc: [%d, %d] \n", gval, lval);
	return 0;
}

四、waitwaitpid 函数

1、wait 函数

wait 函数是阻塞函数,只有任意一个子进程结束,它才能继续往下执行,否则卡住那里等

它获得结束子进程的PID以及 退出状态/退出码 , 并且回收子进程的内存资源

函数原型

1
2
3
#include <sys/wait.h>
 
pid_t wait(int * statloc);

statloc:传出参数, 传出退出状态/ 退出码,用法如下

返回值:返回结束的子进程的 PID,失败返回 -1

1
2
3
4
5
6
7
8
9
10
11
12
WIFEXITED(status);		// 判断子进程是否正常结束
WEXITSTATUS(status);	// 获得子进程的退出码(8位,0-255)
WIFSINGNALED(status);	// 判断子进程是否被信号打断
    

pid1 = wait(&status);//等待任意一个子进程的结束
if(WIFEXITED(status)){
    printf("%d正常结束!退出码 = %d\n",pid1,WEXITSTATUS(status));
}
if(WIFSIGNALED(status)){
    printf("%d被信号打断!信号 = %d\n",pid1,WTERMSIG(status));
}

2、waitpid 函数

waitpid 函数可以指定等待的子进程,还可以选择等待方式(阻塞或者不阻塞)

函数原型

1
2
3
#include <sys/wait.h>

pit_t waitpid(pid_t pid, int * statloc, int options);

pid:等待结束的目标子进程的 ID,若传递 -1则等待任意子进程结束

statloc:传出参数

options:选择等待方式,0 代表 阻塞,WNOHANG 代表非阻塞

返回值:返回结束的子进程的 PID,失败返回 -1

五、信号处理

子进程何时结束是不确定的,一直等待下去是不现实的。所以操作系统会在子进程结束的时候给父进程发送信号,父进程可以设置接收到信号时自动调用自定义的处理函数。

1、signal 函数

函数原型

1
2
3
4
#include <signal.h>

void (*signal(int signo, void (*func)(int)))(int);
// void (*func(int)) 是一种特殊的类型为函数指针

signo:信号类型,传入信号类型对应的宏

  • SIGALRM:调用 alarm 函数注册的事件
  • SIGINT:输入 CTRL + C
  • SIGCHLD:子进程结束

func:处理函数,传入自定义的处理函数

alarm 函数原型

1
2
3
#include <unistd.h>

unsigned int alarm(unsigned int seconds);

示例:

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
#include <stdio.h>
#include <unistd.h>
#include <signal.h>

void timeout(int sig)
{
	if(sig==SIGALRM)
		puts("Time out!");

	alarm(2);	
}
void keycontrol(int sig)
{
	if(sig==SIGINT)
		puts("CTRL+C pressed");
}

int main(int argc, char *argv[])
{
	int i;
	signal(SIGALRM, timeout);
	signal(SIGINT, keycontrol);
	alarm(2);

	for(i=0; i<3; i++)
	{
		puts("wait...");
		sleep(100);
	}
	return 0;
}

调用函数的主体是操作系统,但是进程处于睡眠状态时无法调用函数。因此产生信号时,为了调用信号处理器,将唤醒由于调用 sleep 函数而进入阻塞状态的进程。而且进程一旦被唤醒,就不会再进入睡眠状态。

2、sigaction 函数

sigaction 函数完全可以替代 signal 函数,也更加稳定。sigaction 函数在 UNIX 系列的不同操作系统中完全相同,但是signal 函数可能有区别。

现代编程使用 sigaction 函数更多。

函数原型

1
2
3
4
5
6
7
8
9
#include <signal.h>

int sigaction(int signo, const struct sigaction * act, struct sigaction * oldact);

struct sigaction {
    void (*sa_handler)(int);
    sigset_t sa_mask;
    int sa_flags;
}

signo:信号类型

act:包含信号处理函数信息的结构体

oldact:该信号之前的处理函数信息结构体,若不需要则传递 0

示例:

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
#include <stdio.h>
#include <unistd.h>
#include <signal.h>

void timeout(int sig)
{
	if(sig==SIGALRM)
		puts("Time out!");
	alarm(2);	
}

int main(int argc, char *argv[])
{
	int i;
	struct sigaction act;
	act.sa_handler=timeout;
	sigemptyset(&act.sa_mask);
	act.sa_flags=0;
	sigaction(SIGALRM, &act, 0);

	alarm(2);

	for(i=0; i<3; i++)
	{
		puts("wait...");
		sleep(100);
	}
	return 0;
}

六、网络编程

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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
#include <arpa/inet.h>
#include <sys/socket.h>

#define BUF_SIZE 30
void error_handling(char *message);
void read_childproc(int sig);

int main(int argc, char *argv[])
{
	int serv_sock, clnt_sock;
	struct sockaddr_in serv_adr, clnt_adr;
	
	pid_t pid;
	struct sigaction act;
	socklen_t adr_sz;
	int str_len, state;
	char buf[BUF_SIZE];
	if(argc!=2) {
		printf("Usage : %s <port>\n", argv[0]);
		exit(1);
	}

	act.sa_handler=read_childproc;
	sigemptyset(&act.sa_mask);
	act.sa_flags=0;
	state=sigaction(SIGCHLD, &act, 0);
	serv_sock=socket(PF_INET, SOCK_STREAM, 0);
	memset(&serv_adr, 0, sizeof(serv_adr));
	serv_adr.sin_family=AF_INET;
	serv_adr.sin_addr.s_addr=htonl(INADDR_ANY);
	serv_adr.sin_port=htons(atoi(argv[1]));
	
	if(bind(serv_sock, (struct sockaddr*) &serv_adr, sizeof(serv_adr))==-1)
		error_handling("bind() error");
	if(listen(serv_sock, 5)==-1)
		error_handling("listen() error");
	
	while(1)
	{
		adr_sz=sizeof(clnt_adr);
		clnt_sock=accept(serv_sock, (struct sockaddr*)&clnt_adr, &adr_sz);
		if(clnt_sock==-1)
			continue;
		else
			puts("new client connected...");
		pid=fork();
		if(pid==-1)
		{
			close(clnt_sock);
			continue;
		}
		if(pid==0)
		{
			close(serv_sock);
			while((str_len=read(clnt_sock, buf, BUF_SIZE))!=0)
				write(clnt_sock, buf, str_len);
			
			close(clnt_sock);
			puts("client disconnected...");
			return 0;
		}
		else
			close(clnt_sock);
	}
	close(serv_sock);
	return 0;
}

void read_childproc(int sig)
{
	pid_t pid;
	int status;
	pid=waitpid(-1, &status, WNOHANG);
	printf("removed proc id: %d \n", pid);
}
void error_handling(char *message)
{
	fputs(message, stderr);
	fputc('\n', stderr);
	exit(1);
}

上边是一段并行服务器程序,其中第 60 行关闭了第 33 行创建的服务器套接字,这是因为服务器套接字描述符也传递到了子进程。但是严格意义上,套接字属于操作系统,进程只拥有代表相应套接字的文件描述符。和 C++ 中的智能指针类似,程序中的套接字描述符只是一个对系统中套接字的引用,只有当一个套接字的所有引用都关闭之后,套接字才会真正地关闭。