10.1 进程概念及其应用
10.1.1 并发服务器端的实现方法
由于在网络程序中数据通信时间比CPU运算时间占比更大。因此,向多个客户端提供服务是一种有效利用CPU的方式。
- 多进程服务器:通过创建多个进程提供服务
- 多路复用服务器:通过捆绑并统一管理I/O对象提供服务
- 多线程服务器:通过生成与客户端等量的线程提供服务
10.1.2 理解进程
进程概念:占用内存空间的正在运行的程序。
CPU核数与进程的关系:拥有x个运算设备的CPU称作x核。核的个数与正在运行的进程个数相同。相反,若进程数超过核数,进程将会分时使用CPU资源。但是因为CPU运转速度极快,我们会感到所有进程同时运行。
10.1.2.1 进程ID
无论进程是如何创建的,所有进程都会从操作系统分配到ID。那么其值都是大于2的整数。1要分配给操作系统启动后的(用于协作操作系统)首个进程,因此用户进程无法得到ID值1。
zhang@zhang-virtual-machine:~/Desktop/Ctest$ ps au
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
gdm 1143 0.0 0.1 170280 6092 tty1 Ssl+ 08:36 0:00 /usr/lib/gdm3/gdm-wayland
gdm 1148 0.0 0.0 5300 1048 tty1 S+ 08:36 0:00 dbus-run-session -- gnome
gdm 1149 0.0 0.1 7660 4796 tty1 S+ 08:36 0:00 dbus-daemon --nofork --pr
gdm 1152 0.0 0.4 568504 16808 tty1 Sl+ 08:36 0:00 /usr/libexec/gnome-sessio
gdm 1236 0.0 4.4 3939576 177332 tty1 Sl+ 08:36 0:03 /usr/bin/gnome-shell
gdm 1342 0.0 0.2 309548 8248 tty1 Sl+ 08:36 0:00 /usr/libexec/at-spi-bus-l
gdm 1353 0.0 0.0 7248 3840 tty1 S+ 08:36 0:00 /usr/bin/dbus-daemon --co
gdm 1359 0.0 1.5 1057468 60660 tty1 Sl+ 08:36 0:00 /usr/bin/Xwayland :1024 -
gdm 1379 0.0 0.1 244228 4532 tty1 Sl+ 08:36 0:00 /usr/libexec/xdg-permissi
gdm 1392 0.0 0.1 162772 6748 tty1 Sl+ 08:36 0:00 /usr/libexec/at-spi2-regi
gdm 1394 0.0 0.6 2742308 26880 tty1 Sl+ 08:36 0:00 /usr/bin/gjs /usr/share/g
gdm 1395 0.0 0.3 477408 12324 tty1 Sl+ 08:36 0:00 /usr/libexec/gsd-sharing
gdm 1397 0.0 0.5 423932 23016 tty1 Sl+ 08:36 0:00 /usr/libexec/gsd-wacom
gdm 1404 0.0 0.6 499368 24552 tty1 Sl+ 08:36 0:00 /usr/libexec/gsd-color
gdm 1409 0.0 0.5 350536 23688 tty1 Sl+ 08:36 0:00 /usr/libexec/gsd-keyboard
gdm 1411 0.0 0.2 330584 11252 tty1 Sl+ 08:36 0:00 /usr/libexec/gsd-print-no
gdm 1415 0.0 0.1 465732 6096 tty1 Sl+ 08:36 0:00 /usr/libexec/gsd-rfkill
gdm 1420 0.0 0.2 326584 10836 tty1 Sl+ 08:36 0:00 /usr/libexec/gsd-smartcar
gdm 1424 0.0 0.4 382872 17288 tty1 Sl+ 08:36 0:00 /usr/libexec/gsd-datetime
gdm 1428 0.0 0.6 906096 25172 tty1 Sl+ 08:36 0:00 /usr/libexec/gsd-media-ke
gdm 1430 0.0 0.1 244140 5928 tty1 Sl+ 08:36 0:00 /usr/libexec/gsd-screensa
gdm 1435 0.0 0.2 330292 11240 tty1 Sl+ 08:36 0:00 /usr/libexec/gsd-sound
gdm 1439 0.0 0.2 322424 9320 tty1 Sl+ 08:36 0:00 /usr/libexec/gsd-a11y-set
gdm 1445 0.0 0.2 322660 9632 tty1 Sl+ 08:36 0:00 /usr/libexec/gsd-housekee
gdm 1447 0.0 0.6 498924 24500 tty1 Sl+ 08:36 0:00 /usr/libexec/gsd-power
gdm 1475 0.0 0.3 350824 15196 tty1 Sl+ 08:36 0:00 /usr/libexec/gsd-printer
gdm 1522 0.0 0.2 396892 10452 tty1 Sl 08:36 0:00 ibus-daemon --panel disab
gdm 1533 0.0 0.2 248740 8600 tty1 Sl 08:36 0:00 /usr/libexec/ibus-dconf
gdm 1536 0.0 1.3 337904 51880 tty1 Sl 08:36 0:00 /usr/libexec/ibus-x11 --k
gdm 1540 0.0 0.2 248720 9028 tty1 Sl+ 08:36 0:00 /usr/libexec/ibus-portal
gdm 1541 0.0 0.2 175044 8776 tty1 Sl 08:36 0:00 /usr/libexec/ibus-engine-
zhang 1979 0.0 0.1 19248 5088 pts/3 Ss 08:37 0:00 /usr/bin/bash
zhang 7835 0.0 0.0 20132 3612 pts/3 R+ 10:26 0:00 ps au
10.1.2.2 通过调用fork函数创建进程
#include<unistd.h>
pid_t fork(void);
//成功时返回进程ID,失败时返回-1
//改函数通过调用复制正在运行的调用fork函数的进程。另外两个进程都将执行fork函数调用后的语句,但因为通过同一个进程,复制相同的内存空间,之后的程序要根据fork函数的返回值加以区分。
- 父进程:fork函数返回子进程ID
- 子进程:fork函数返回0
父进程是指原进程,这里的子进程是父进程通过调用fork函数复制出来的进程。
//fork.c
#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)
gval+=2, lval+=2;
else
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;
}
zhang@zhang-virtual-machine:~/Desktop/Ctest/Demo11$ ./fork
Parent Proc: [9, 23]
Child Proc: [13, 27]
10.2 进程和僵尸进程
10.2.1 产生僵尸进程的原因
- 传递参数并调用exit函数
- main函数中执行return语句并返回值
向exit函数传递的参数值和main函数的return语句返回的值都会传递给操作系统,操作系统不会销毁子进程,直到把这些值传递给产生孩子进程的父进程。如果父进程未主动要求获得子进程的结束状态值,操作系统将一直保存,所以会产生僵尸进程。
//zombie.c
#include<stdio.h>
#include<unistd.h>
int main(int argc, char *argv[])
{
pid_t pid=fork();
if(pid==0)
{
puts("Hi, I am a child process");
}
else
{
printf("Child Process ID: %d \n", pid);
sleep(30);
}
if(pid==0)
{
puts("End child process");
}
else
{
puts("End parent process");
}
return 0;
}
zhang@zhang-virtual-machine:~/Desktop/Ctest$ ps au
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
zhang 2001 0.0 0.0 2496 524 pts/0 S+ 11:56 0:00 ./zombie
zhang 2002 0.0 0.0 0 0 pts/0 Z+ 11:56 0:00 [zombie] <defunct>
zhang 2039 0.0 0.1 19248 5204 pts/1 Ss 11:56 0:00 /usr/bin/bash
zhang 2058 0.0 0.0 20132 3524 pts/1 R+ 11:56 0:00 ps au
补充:后台处理
首先定义后台处理,后台处理是指将控制台窗口的指令放到后台运行的方式。
zhang@zhang-virtual-machine:~/Desktop/Ctest/Demo12$ ./zombie &
[1] 2132
zhang@zhang-virtual-machine:~/Desktop/Ctest/Demo12$ Child Process ID: 2133
Hi, I am a child process
End child process
ps au
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
zhang 1938 0.0 0.1 19376 5248 pts/0 Ss 11:55 0:00 /usr/bin/bash
zhang 2132 0.0 0.0 2496 584 pts/0 S 11:57 0:00 ./zombie
zhang 2133 0.0 0.0 0 0 pts/0 Z 11:57 0:00 [zombie] <defunct>
zhang 2170 0.0 0.0 20132 3552 pts/0 R+ 11:58 0:00 ps au
zhang@zhang-virtual-machine:~/Desktop/Ctest/Demo12$ End parent process
10.2.2 销毁僵尸进程1:利用wait函数
#include<sys/wait.h>
pid_t wait(int *statloc);
//成功时返回进程ID,失败时返回-1
//将子进程终止时传递的返回值保存到函数参数所指向的内存空间
- WIFEXITED子进程正常终止返回“真”
- WEXITSTATUS返回子进程的返回值
//wait.c
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/wait.h>
int main(int argc, char *argv[])
{
int status;
pid_t pid=fork();
if(pid==0)
{
return 3;
}
else
{
printf("Child PID: %d \n", pid);
pid=fork();
if(pid==0)
{
exit(7);
}
else
{
printf("Child PID: %d \n", pid);
wait(&status);
if(WIFEXITED(status))
printf("Child send one: %d \n", WEXITSTATUS(status));
wait(&status);
if(WIFEXITED(status))
printf("Child send two: %d \n", WEXITSTATUS(status));
sleep(30);
}
}
return 0;
}
zhang@zhang-virtual-machine:~/Desktop/Ctest/Demo13$ ./wait
Child PID: 14725
Child PID: 14726
Child send one: 3
Child send two: 7
10.2.3 销毁僵尸进程2:使用waitpid函数
由于wait函数会引起程序的阻塞,所以要考虑调用更好的方式来防止这样的发生,这就需要引进waitpid函数,这是防止僵尸进程的第二种方法。
#include<sys/wait.h>
pid_t waitpid(pid_t pid, int *statloc, int options);
//成功时返回终止子进程的ID,失败时返回-1
//pid 等待终止的目标子进程的ID,若传递-1,则与wait函数相同,可以等待任意子进程的终止
//statloc与wait函数相同的含义
//options传递头文件sys/wait.h中声明的常量WNOHANG,即使没有终止的子进程也不会进入阻塞状态,而是返回0并且退出
//waitpid.c
#include<stdio.h>
#include<unistd.h>
#include<sys/wait.h>
int main(int argc, char *argv[])
{
int status;
pid_t pid=fork();
if(pid==0)
{
sleep(15);
return 24;
}
else
{
while(!waitpid(-1, &status, WNOHANG))
{
sleep(3);
puts("sleep 3sec.");
}
if(WIFEXITED(status))
printf("Child send %d \n", WEXITSTATUS(status));
}
return 0;
}
zhang@zhang-virtual-machine:~/Desktop/Ctest/Demo14$ gcc waitpid.c -o waitpid
zhang@zhang-virtual-machine:~/Desktop/Ctest/Demo14$ ./waitpid
sleep 3sec.
sleep 3sec.
sleep 3sec.
sleep 3sec.
sleep 3sec.
Child send 24
10.3 信号处理
由于上节提到的调用waitpid函数来终止子进程,但是又引申出另外一个问题就是子进程什么时候终止,如果不终止会不会一直等待,这样会占用处理机的资源。那么根据问题,下面提出几个解决方案。
10.3.1 向操作系统求助
子进程终止的识别主体是操作系统,所以由操作系统来告诉父进程最为合适。所以需要引入信号量机制。
10.3.2 信号与signal函数
“注册信号”过程,就是父进程发现自己的子进程结束时,请求操作系统调用特定的函数。
#include<signal.h>
void (*signal(int signo, void (*func)(int)))(int);//可以理解为函数指针的嵌套
//为了在产生信号时调用,返回之前注册的函数指针
- 函数名:signal
- 参数:int signo, **void ( func)(int)
- 返回类型:参数为int型,返回void型函数指针
第一个参数为特殊情况信息,第二个参数是为特殊情况下将要调用的函数地址值,signal函数中注册的部分特殊情况和对应的常数,如下所示:
- SIGALRM:已到通过调用alarm函数注册的时间
- SIGINT:输入Ctrl+C
- SIGCHLD:子进程终止
#include<unistd.h>
unsigned int alarm(unsigned int seconds);
//返回0或以秒为单位的距SIGALRM信号发生所剩时间
//signal.c
#include<stdio.h>
#include<unistd.h>
#include<signal.h>
//5~11为信号处理器函数
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函数的时候,进程会进入阻塞状态,一旦进入该状态,想要唤醒就需要传递操作系统可以解析的参数,从而唤醒该进程,因此之后的进程就不会处于阻塞态,进程一旦被唤醒就不会在进入睡眠状态。
10.3.3 利用sigaction函数进行信号处理
sigaction函数与之前的waitpid函数类似,都是在对之前的函数基础之上,出现了更加稳定,更加安全的函数。另外因为在unix操作系中,signal函数可能会存在不同的差异,但是sigaction函数完全相同。signal函数只是保持了对旧版本的兼容。
#include<signal.h>
int sigaction(int signo, const struct sigaction *act, struct sigaction *oldact);
//成功时返回0,失败时返回-1
//signo与signal函数相同,传递信号信息
//对应于第一个参数的信号处理函数(信号处理器)信息
//通过此参数获取之前注册的信号处理函数指针,若不需要则传递0
//sigaction结构体
struct sigaction
{
void (*sa_handler)(int);//sa_handler保存信号处理函数的指针值
sigset_t sa_mask;//以下两个是指定信号的相关选项和特性
int sa_flags;
}
//sigaction.c
//sigaction.c
#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;
}
10.3.4 利用信号处理技术消灭僵尸进程
//remove_zombie.c
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<signal.h>
#include<sys/wait.h>
void read_childproc(int sig)
{
int status;
pid_t id=waitpid(-1, &status, WNOHANG);
if(WIFEXITED(status))
{
printf("Remove proc id: %d \n", id);
printf("Child send: %d \n", WEXITSTATUS(status));
}
}
int main(int argc, char *argv[])
{
pid_t pid;
struct sigaction act;
act.sa_handler=read_childproc;
sigemptyset(&act.sa_mask);
act.sa_flags=0;
sigaction(SIGCHLD, &act, 0);
pid=fork();
if(pid==0) /* 子进程执行区域 */
{
puts("Hi! I'm child process");
sleep(10);
return 12;
}
else /* 父进程执行区域 */
{
printf("Child proc id: %d \n", pid);
pid=fork();
if(pid==0) /* 另一个子进程执行区域 */
{
puts("Hi! I'm child process");
sleep(10);
exit(24);
}
else
{
int i;
printf("Child proc id: %d \n", pid);
for(i=0; i<5; i++)
{
puts("wait...");
sleep(5);
}
}
}
return 0;
}
zhang@zhang-virtual-machine:~/Desktop/Ctest/Demo17$ ./remove_zombie
Child proc id: 5160
Hi! I'm child process
Child proc id: 5161
wait...
Hi! I'm child process
wait...
wait...
Remove proc id: 5160
Child send: 12
wait...
wait...
10.4 基于多任务的并发服务器
10.4.1 基于进程的并发服务器模型
每当有客户端请求服务时,回声客户端都创建子进程以提供服务。
- 第一阶段:回声服务器(父进程)调用accept函数受理链接请求
- 第二阶段:此时获取的套接字文件描述符创建并传递给子进程
- 第三阶段:子进程利用传递来的文件描述符提供服务(实际上并不需要此步骤,因为子进程会复制父进程的所有资源,根本不需要传递文件描述符)
10.4.2 实现并发服务器
//echo_mpserv.c
#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);
}
#server
zhang@zhang-virtual-machine:~/Desktop/Ctest/Demo18$ ./echo_mpserv 9190
new client connected...
new client connected...
#client one
zhang@zhang-virtual-machine:~/Desktop/Ctest$ ./client 127.0.0.1 9190
Connected.........
Input message(Q to quit):
#client two
zhang@zhang-virtual-machine:~/Desktop/Ctest$ ./client 127.0.0.1 9190
Connected.........
Input message(Q to quit):
10.5 分割TCP的I/O程序
10.5.1 回声客户端的I/O程序分割
//echo_mpclient.c
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<arpa/inet.h>
#include<sys/socket.h>
#define BUF_SIZE 30
void error_handling(char *message);
void read_routine(int sock, char *buf);
void write_routine(int sock, char *buf);
int main(int argc, char *argv[])
{
int sock;
pid_t pid;
char buf[BUF_SIZE];
struct sockaddr_in serv_adr;
if(argc!=3){
printf("Usage : %s <IP> <port>\n", argv[0]);
exit(1);
}
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=inet_addr(argv[1]);
serv_adr.sin_port=htons(atoi(argv[2]));
if(connect(sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr))==-1)
error_handling("connect() error!");
pid=fork();
if(pid==0)
write_routine(sock, buf);
else
read_routine(sock, buf);
close(sock);
return 0;
}
void read_routine(int sock, char *buf)
{
while(1)
{
int str_len=read(sock, buf, BUF_SIZE);
if(str_len==0)
return;
buf[str_len]=0;
printf("Message from server: %s ", buf);
}
}
void write_routine(int sock, char *buf)
{
while(1)
{
fgets(buf, BUF_SIZE, stdin);
if(!strcmp(buf, "q\n")||!strcmp(buf, "Q\n"))
{
shutdown(sock, SHUT_WR);
return;
}
write(sock, buf, strlen(buf));
}
}
void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}