蒋迪的博客
| 关于 | 归档

#网络编程 TCP/IP网络编程笔记 Part II

TCP/IP网络编程笔记 Part II

网络编程进阶 1

并发服务器

Q&A

什么是进程?如何产生进程?

什么是僵尸进程?如何处理僵尸进程?

What is a Zombie Process:

When a process dies on Linux, it isn’t all removed from memory immediately — its process descriptor stays in memory (the process descriptor only takes a tiny amount of memory). The process’s status becomes EXIT_ZOMBIE and the process’s parent is notified that its child process has died with the SIGCHLD signal. The parent process is then supposed to execute the wait() system call to read the dead process’s exit status and other information. This allows the parent process to get information from the dead process. After wait() is called, the zombie process is completely removed from memory. However, if a parent process isn’t programmed properly and never calls wait(), its zombie children will stick around in memory until they’re cleaned up.


Dangers of Zombie Processes:

Zombie processes don’t use up any system resources. (Actually, each one uses a very tiny amount of system memory to store its process descriptor.) However, each zombie process retains its process ID (PID). Linux systems have a finite number of process IDs – 32767 by default on 32-bit systems. If zombies are accumulating at a very quick rate – for example, if improperly programmed server software is creating zombie processes under load — the entire pool of available PIDs will eventually become assigned to zombie processes, preventing other processes from launching.

如前所述,为了销毁僵尸子进程,父进程应该主动请求获取子进程的返回值。通常存在两种办法。

  1. 利用wait函数
  2. 利用waitpid函数
#include <sys/wait.h>
pid_t wait(int * statloc)

利用此函数时如果已有子进程终止,那么子进程终止的返回值将保存到该函数的参数所指向的内存空间。一般可以通过如下宏进行分离。

  • WIFEXITED 正常终止时返回 “真”
  • WEXITSTAUS 返回子进程的返回值
if(WIFEITED(status)){// 时否正常终止?
    puts("Normal termination !");
    printf("child pass num: %d",WEXITSTATUS(status));//返回值
}

```c
#include <stdio.h>
#include <stlib.h>
#include <unistd.h>
#include <sys/wait.h>

int main(int argc , char *argv[]){
    pid_t pNum;
    int status;
    
    pNum=fork();
    if(pNum == 0)
    {
        printf("This is child Process\n");
    }
    else{
        sleep(15);
        printf("This is parent Process\n");
        
    }
    
    if(pNum == 0)
    {
        printf("Child Process Ends");
        return 3;
    }
    else
    {
        wait(&status);
        if(WIFEITED(status)){
            puts("Normal termination !");
            printf("child pass num: %d",WEXITSTATUS(staus));
        }
        printf("parent process Ends");
    }

    return 0;
}

以上便是通过wait函数消灭僵尸进程的方法,调用wait函数时,如果没有已经终止的子进程,那么程序将阻塞(Blocking)直到有子进程终止。所以,这里引出了第二种方法。

#include <sys/wait.h>
pid_t waitpid(pid_t , int *staloc, int options)
/* 
pid 等待终止的目标子进程PID,若传递-1,则与wait函数相同。可传递任何子进程。
staloc 
options WNOHANG 即使没有终止的子进程, 也不会进入阻塞状态。
*/

通过调用上述函数,可以正常销毁僵尸进程。但是父进程往往不可能一直等着子进程结束,所以我们可以使用信号来帮助我们。

#include <signal.h>
void (* signal(int signo,void(*func)(int)))(int);
  • 函数名 为signal.
  • 参数 int signo, void(*func)(int)
  • 返回类型:参数为int型,返回void型指针

  • SIGALRM:已通过调用alarm函数注册的时间
  • SIGINT:输入CTRL+C
  • SIGCHLD:子进程终止
#include <signal.h>
int sigatction(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
#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("Removed proc id %d \n",id);
        printf("Chils 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(SIGHLD,&act,0);

    pif = fork();

    if(pid == 0){
        puts("Hi I'm child process");
        sleep(10);
        return 12;
    }
    else
    {
        printf("Hi I'm parent process");
        sleep(30);
    }
    return 0;
}

如何构建基于多进程的并发处理器?

实现并发服务器

while(1){

    clnt_sock=accept(serv_sock,(struct sockaddr*)&clnt_adr,&adr_sz);
    pid=fork();
    if(pid==0){// 子程序运行区域
        puts("client connected\n");
        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\n");
    }
    else
    {
        close(clnt_sock);
    }
}

多进程服务端的缺点?

  • 创建进程时需要付出极大的代价。需要使用大量的内存和进行大量的计算。

    为什么要分割IO?

    1. 程序实现更加简单
    2. 提高数据交换的性能

进程通信 IPC

进程通信,意味着两个不同的进程可以交换数据。对于具体的实现来说,这意味着两个进程可以同时访问的内存空间。

Q&A

如何通过管道实现进程间的通信?管道如何实现进程间的双向通信?

IO复用

Q&A

在网络编程中,如何理解复用IO?

复用IO的提出是为了弥补多进程服务器的缺陷而诞生,这使得只需使用一个进程,而可以服务多个客户端。大大的减少了多进程并发服务端的进程数,降低了系统的开销。

如何理解select函数?

This function is somewhat strange, but it’s very useful. Take the following situation: you are a server and you want to listen for incoming connections as well as keep reading from the connections you already have.select() gives you the power to monitor several sockets at the same time. It’ll tell you which ones are ready for reading, which are ready for writing, and which sockets have raised exceptions.

#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
int select(int numfds, fd_set *readfds, fd_set *writefds,
    fd_set *exceptfds, struct timeval *timeout); 

Note:The parameter numfds should be set to the values of the highest file descriptor plus one.If you want to see if you can read from standard input and some socket descriptor, sockfd, just add the file descriptors 0 and sockfd to the set readfds. it should be set to sockfd+1, since it is assuredly higher than standard input (0).


When select() returns, readfds will be modified to reflect which of the file descriptors you selected which is ready for reading. U can test them with the FD_ISSET().


|Function|Description| |—|—| |FD_SET(int fd, fd_set *set)|Add fd to the set.| |FD_CLR(int fd, fd_set *set)|Remove fd from the set.| |FD_ISSET(int fd, fd_set *set)|Return true if fd is in the set.| |FD_ZERO(fd_set *set)|Clear all entries from the set.|

#include <stdio.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>


int main(){
    struct timeval tv;
    fd_set readfds;
    tv.tv_sec=2
    tv.tv_usec=500000;

    FD_ZERO(&readfds);
    FD_SET(STDIN,&readfds);

    select(STDIN+1,&readfds,NULL,NULL,&tv);

    if(FD_ISSET(STDIN,&readfds))
        printf("A key was pressed!\n");
    else 
        printf("Time out\n");


    return 0;
}

The select() function allows you to implement an event driven design pattern, when you have to deal with multiple event sources.

Let’s say you want to write a program that responds to events coming from several event sources e.g. network (via sockets), user input (via stdin), other programs (via pipes), or any other event source that can be represented by an fd. You could start separate threads to handle each event source, but you would have to manage the threads and deal with concurrency issues. The other option would be to use a mechanism where you can aggregate all the fd into a single entity fdset, and then just call a function to wait on the fdset. This function would return whenever an event occurs on any of the fd. You could check which fd the event occurred on, read that fd, process the event, and respond to it. After you have done that, you would go back and sit in that wait function - till another event on some fd arrives.

select facility is such a mechanism, and the select() function is the wait function. You can find the details on how to use it in any number of books and online resources.

如何使用select函数实现复用IO的并发服务端?

多播与广播

Reference