<TCP/IP网络编程> Chap10. 多进程服务器端

进程概念及应用

并发服务器端实现模型和方法:

  • 多进程服务器(Chap10&11)
  • 多路复用服务器(Chap12)
  • 多线程服务器(Chap18)

进程:占用内存空间的正在运行的程序。

1个CPU中可能包含多个运算设备(核)。核的个数与可同时运行的进程数相同。
可以通过调用fork函数创建进程。

#include <unistd.h>
pid_t fork(void);    // 成功时返回进程ID,失败时返回-1

fork函数复制正在运行的父进程,父子进程都将执行fork函数之后的语句。但父进程的fork返回值是子进程ID,子进程的fork返回值是0,应利用此特点区分后续代码的执行流程。

# gcc fork.c -o fork
# ./fork 
Parent Proc: [9, 23] 
Child Proc: [13, 27]


进程和僵尸进程

下面的示例中,子进程先return(或exit),但父进程不知道,子进程就变成了僵尸进程(Z+)。直到父进程终止,子进程才会与父进程一起被销毁。

# ./zombie &
[1] 19154
Child Process ID: 19156 
Hi, I am a child process
# ps au
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
...
root     19154  0.0  0.0   4356   732 pts/3    S    06:48   0:00 ./zombie
root     19156  0.0  0.0      0     0 pts/3    Z    06:48   0:00 [zombie] <defunct>
# ps au
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
...
[1]+  Done                    ./zombie

为了销毁子进程,父进程应主动请求获取子进程的返回值。方法之一就是调用如下函数:

#include <sys/wait.h>
pid_t wait(int *statloc);    // 成功时返回终止的子进程ID,失败时返回-1

调用此函数时,如果子进程已终止,那么子进程return的返回值(或exit函数的参数值)将保存到statloc指向的内存空间。statloc还包含其他信息,可以用相应的宏来判断。

# gcc wait.c -o wait
# ./wait &
[1] 5586
Child PID: 5588 
Child PID: 5595 
Child send 1: 3 
Child send 1: 7 
root@nsx:~/tcpip/Chap10 [master] # ps au
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
...

注意,wait并没有传入子进程ID,它是按照子进程结束(而非创建)的顺序返回结果的。如果没有已终止的子进程,父进程会在这里阻塞。
第二个主动请求获取子进程返回值的方法:

#include <sys/wait.h>
/* 
 * @params
 *   pid: 子进程ID,若传递-1,则与wait函数相同
 *   statloc: 同wait函数中的statloc
 *   options: 传递头文件sys/wait.h中声明的常量WNOHANG,即使没有终止的子进程也不会阻塞,而是返回0并退出函数
 */
pid_t waitpid(pid_t pid, int *statloc, int options);    // 成功时返回终止的子进程ID,失败时返回-1

waitpid不会阻塞父进程,因此可以循环检查,并在期间处理别的工作。

# gcc waitpid.c -o waitpid
# ./waitpid 
Do sth else.
Do sth else.
Do sth else.
Do sth else.
Do sth else.
Do sth else.
Child send: 24


信号处理

信号是在特定事件发生时由操作系统向进程发送的消息。

#include <signal.h>
/* 
 * @params
 *   signo: 特殊情况信息(如SIGALRM代表已到通过alarm函数注册的时间,SIGINT代表输入CTRL+C,SIGCHILD代表子进程终止)
 *   func: 发生该特殊情况时要调用的函数的指针
 */
void (*signal(int signo, void (*func)(int)))(int);

这个函数的返回值为函数指针,指向signal函数调用之前的信号处理函数(第二次调用signal函数的时候,它的返回值就是第一次调用signal时传入的信号处理函数)。

#include <unistd.h>
unsigned int alarm(unsigned int seconds);    // 返回0或以秒为单位的距SIGALRM信号发生所剩的时间

若alarm函数的参数为0代表取消之前对SIGALRM信号的预约。

# gcc signal.c -o signal
# ./signal
Wait...
^CCTRL+C pressed
Wait...
Time out!
Wait...
^CCTRL+C pressed

我们观察到尽管主进程的循环内有100秒的sleep函数,但仍很快就结束了。因为每一个到达的信号都唤醒了主进程,使其离开阻塞状态以处理信号。
不过signal函数在UNIX系列的不同操作系统中可能存在区别,可以用sigaction函数来完全替代它。

#include <signal.h>
struct sigaction {
    void (*sa_handler)(int);
    sigset_t sa_mask;
    int sa_flags;
}
/* 
 * @params
 *   signo: 特殊情况信息,与signal函数相同
 *   act: 信号处理函数信息
 *   oldact: 获取之前的信号处理函数指针,若不需要则传递0
 */
int sigaction(int signo, const struct sigaction *act, struct sigaction *oldact);    // 成功时返回0, 失败时返回-1

它的作用可以等同于signal函数:

# gcc sigaction.c -o sigaction
# ./sigaction 
Wait...
Time out!
Wait...
Time out!
Wait...
Time out!

利用sigaction函数消灭僵尸进程:

# gcc remove_zombie.c -o rmzombie
# ./rmzombie 
Child proc id: 9351 
Child proc id: 9352 
Wait...
I am child process
I am child process
Wait...
Enter signal handler...Remove proc id: 9352 
Child send: 24 
Enter signal handler...Remove proc id: 9351 
Child send: 12 
Wait...
Wait...
Wait...

我修改了原书中的例子,在waitpid函数处加了循环,因为两个子进程终止的时间太过于接近导致waitpid函数只处理了一个信号(两个信号都在信号处理函数执行之前产生)。

基于多任务的并发服务器

每当有客户端请求链接时,服务器端都创建子进程以提供服务。可以结合第四章的回声客户端来运行。

# gcc echo_mpserv.c -o mpserv
# ./mpserv 9190
Connected client 1 
Begin exchanging data with client 1 
Connected client 2 
Begin exchanging data with client 2 
Client disconnected...
Remove proc id: 30324 
Child send: 0 
Client disconnected...
Remove proc id: 30496 
Child send: 0

第一次写完这段程序时,我杯具地发现退出其中一个客户端时,服务器开始刷屏Remove proc id: 0,因为我在前文把waitpid函数放在了while循环里,而它却一直收到0返回值因此不能退出循环。实际上0是期待的返回值,因为这个时候还有其他客户端正在连接(意味着还有其他子进程未结束),waitpid就会返回0。-1是出现错误时的返回值(如已经没有子进程了)。前一个例子之所以没有发现这个错误是因为两个子进程都在信号处理函数前就终止了,没有给waitpid函数返回0的机会。
修改循环的判定条件为大于0,运行成功,结果如上所示。

分割TCP的I/O程序

客户端的父进程负责接收数据,创建子进程发送数据。

# gcc echo_mpclient.c -o mpclnt
# ./mpclnt 127.0.0.1 9190
Connected
dear
Message from server: dear
Q


习题

  1. 下列关于进程的说法错误的是?
    a. 从操作系统的角度上说,进程是程序运行的单位。
    b. 进程根据创建方式建立父子关系。
    c. 进程可以包含其他进程,即一个进程的内存空间可以包含其他进程。
    d. 子进程可以创建其他子进程,而创建出来的子进程还可以创建其子进程,但所有这些进程只与一个父进程建立父子关系。
    cd。
  2. 调用fork函数将创建子进程,以下关于子进程的描述错误的是?
    a. 父进程销毁时也会同时销毁子进程。
    b. 子进程是复制父进程所有资源创建出的进程。
    c. 父子进程共享全局变量。
    d. 通过fork函数创建的子进程将执行从开始到fork函数调用为止的代码。
    cd。不考虑主动把子进程变成守护进程的情况,父进程销毁时也会同时销毁子进程。子进程完全复制了父进程的资源,包括进程上下文、代码区、数据区、堆区、栈区、内存信息、打开文件的文件描述符、信号处理函数、进程优先级、进程组号、当前工作目录、根目录、资源限制和控制终端等信息,而子进程与父进程的区别有进程号、资源使用情况和计时器等。全局变量位于数据区,也会被一起复制到子进程的内存空间。通过fork函数创建的子进程将执行从开始调用fork函数到最后的代码。
  3. 创建子进程时将复制父进程的所有内容,此时的复制对象也包含套接字文件描述符。编写程序验证复制的文件描述符整数值是否与原文件描述符整数值相同。
    与echo_client搭配运行。
# gcc sockid.c -o sockid
# ./sockid 9190
Connected client 1 
server sock: 3, client sock: 4
server sock: 3, client sock: 4
  1. 请说明进程变为僵尸进程的过程及预防措施。
    如果在父进程中没有注册信号处理函数的话,子进程的退出信号会传递给操作系统,但操作系统并不会销毁子进程,需要传递给父进程去销毁。父进程可以主动发起请求,获得子进程的结束状态值,然后调用wait或waitpid函数来正常终止子进程。
  2. 如果在未注册SIGINT信号的情况下输入Ctrl+C,将由操作系统默认的时间处理器终止程序。但如果直接注册Ctrl+C信号的处理器,则程序不会终止,而是调用程序员指定的事件处理器。编写注册处理函数的程序,完成如下功能:“输入Ctrl+C时,询问是否确定退出程序,输入Y则终止程序”。另外,编写程序使其每隔1秒输出简单字符串,并适用于上述时间处理器注册代码。
# gcc sigint.c -o sigint
# ./sigint 
Waiting
Waiting
^CPressed CTRL+C. Quit? Y/N
N
Waiting
Waiting
Waiting
^CPressed CTRL+C. Quit? Y/N
Y


我的问题

进程号为1的进程是什么?
init进程。

附录

  1. Github
  2. signal函数的返回值究竟是什么?
  3. 处理僵死进程
  4. linux的 0号进程 和 1 号进程
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。