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

  1. zhang@zhang-virtual-machine:~/Desktop/Ctest$ ps au
  2. USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
  3. gdm 1143 0.0 0.1 170280 6092 tty1 Ssl+ 08:36 0:00 /usr/lib/gdm3/gdm-wayland
  4. gdm 1148 0.0 0.0 5300 1048 tty1 S+ 08:36 0:00 dbus-run-session -- gnome
  5. gdm 1149 0.0 0.1 7660 4796 tty1 S+ 08:36 0:00 dbus-daemon --nofork --pr
  6. gdm 1152 0.0 0.4 568504 16808 tty1 Sl+ 08:36 0:00 /usr/libexec/gnome-sessio
  7. gdm 1236 0.0 4.4 3939576 177332 tty1 Sl+ 08:36 0:03 /usr/bin/gnome-shell
  8. gdm 1342 0.0 0.2 309548 8248 tty1 Sl+ 08:36 0:00 /usr/libexec/at-spi-bus-l
  9. gdm 1353 0.0 0.0 7248 3840 tty1 S+ 08:36 0:00 /usr/bin/dbus-daemon --co
  10. gdm 1359 0.0 1.5 1057468 60660 tty1 Sl+ 08:36 0:00 /usr/bin/Xwayland :1024 -
  11. gdm 1379 0.0 0.1 244228 4532 tty1 Sl+ 08:36 0:00 /usr/libexec/xdg-permissi
  12. gdm 1392 0.0 0.1 162772 6748 tty1 Sl+ 08:36 0:00 /usr/libexec/at-spi2-regi
  13. gdm 1394 0.0 0.6 2742308 26880 tty1 Sl+ 08:36 0:00 /usr/bin/gjs /usr/share/g
  14. gdm 1395 0.0 0.3 477408 12324 tty1 Sl+ 08:36 0:00 /usr/libexec/gsd-sharing
  15. gdm 1397 0.0 0.5 423932 23016 tty1 Sl+ 08:36 0:00 /usr/libexec/gsd-wacom
  16. gdm 1404 0.0 0.6 499368 24552 tty1 Sl+ 08:36 0:00 /usr/libexec/gsd-color
  17. gdm 1409 0.0 0.5 350536 23688 tty1 Sl+ 08:36 0:00 /usr/libexec/gsd-keyboard
  18. gdm 1411 0.0 0.2 330584 11252 tty1 Sl+ 08:36 0:00 /usr/libexec/gsd-print-no
  19. gdm 1415 0.0 0.1 465732 6096 tty1 Sl+ 08:36 0:00 /usr/libexec/gsd-rfkill
  20. gdm 1420 0.0 0.2 326584 10836 tty1 Sl+ 08:36 0:00 /usr/libexec/gsd-smartcar
  21. gdm 1424 0.0 0.4 382872 17288 tty1 Sl+ 08:36 0:00 /usr/libexec/gsd-datetime
  22. gdm 1428 0.0 0.6 906096 25172 tty1 Sl+ 08:36 0:00 /usr/libexec/gsd-media-ke
  23. gdm 1430 0.0 0.1 244140 5928 tty1 Sl+ 08:36 0:00 /usr/libexec/gsd-screensa
  24. gdm 1435 0.0 0.2 330292 11240 tty1 Sl+ 08:36 0:00 /usr/libexec/gsd-sound
  25. gdm 1439 0.0 0.2 322424 9320 tty1 Sl+ 08:36 0:00 /usr/libexec/gsd-a11y-set
  26. gdm 1445 0.0 0.2 322660 9632 tty1 Sl+ 08:36 0:00 /usr/libexec/gsd-housekee
  27. gdm 1447 0.0 0.6 498924 24500 tty1 Sl+ 08:36 0:00 /usr/libexec/gsd-power
  28. gdm 1475 0.0 0.3 350824 15196 tty1 Sl+ 08:36 0:00 /usr/libexec/gsd-printer
  29. gdm 1522 0.0 0.2 396892 10452 tty1 Sl 08:36 0:00 ibus-daemon --panel disab
  30. gdm 1533 0.0 0.2 248740 8600 tty1 Sl 08:36 0:00 /usr/libexec/ibus-dconf
  31. gdm 1536 0.0 1.3 337904 51880 tty1 Sl 08:36 0:00 /usr/libexec/ibus-x11 --k
  32. gdm 1540 0.0 0.2 248720 9028 tty1 Sl+ 08:36 0:00 /usr/libexec/ibus-portal
  33. gdm 1541 0.0 0.2 175044 8776 tty1 Sl 08:36 0:00 /usr/libexec/ibus-engine-
  34. zhang 1979 0.0 0.1 19248 5088 pts/3 Ss 08:37 0:00 /usr/bin/bash
  35. 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);
}