《TCP/IP网络编程》学习笔记 | Chapter 10:多进程服务器端

news2024/11/14 10:59:00

《TCP/IP网络编程》学习笔记 | Chapter 10:多进程服务器端

  • 《TCP/IP网络编程》学习笔记 | Chapter 10:多进程服务器端
    • 进程概念及应用
      • 并发服务端的实现方法
      • 理解进程
      • 进程ID
      • 通过调用 fork 函数创建进程
    • 进程和僵尸进程
      • 僵尸进程
      • 产生僵尸进程的原因
      • 销毁僵尸进程 1:利用 wait 函数
      • 销毁僵尸进程 2:使用 waitpid 函数
    • 信号处理
      • 信号与 signal 函数
      • 利用 siganction 函数进行信号处理
      • 利用信号处理技术消灭僵尸进程
    • 基于多任务的并发服务器
      • 基于进程的并发服务器模型
      • 实现并发服务器
      • 通过 fork 函数复制文件描述符
    • 分割 TCP 的 I/O 程序
      • 分割 I/O 程序的优点
      • 回声客户端的 I/O 程序分割
    • 习题
      • (1)下列关于进程的说法错误的是?
      • (2)调用fork函数将创建子进程,以下关于子进程描述错误的是?
      • (3)创建子进程时将复制父进程的所有内容,此时的复制对象也包含套接字文件描述符。编写程序验证复制的文件描述符整数值是否与原文件描述符整数值相同。
      • (4)请说明进程变为僵尸进程的过程及预防措施​。
      • (5)如果在未注册SIGINT信号的情况下输入CtrI+C,将由操作系统默认的事件处理器终止程序。但如果直接注册CtrI+C信号的处理器,则程序不会终止,而是调用程序员指定的事件处理器。编写注册处理函数的程序,完成如下功能:“输入CtrI+C时,询问是否确定退出程序,输入Y则终止程序”。另外,编写程序使其每隔1秒输出简单字符串,并适用于上述时间处理器注册代码。

《TCP/IP网络编程》学习笔记 | Chapter 10:多进程服务器端

进程概念及应用

为了提高客户端的平均满意度,通常会使用并发服务器的方法,并且网络程序中数据通信时间比 CPU 运算时间大,因此向多个客户端提供服务是一种有效利用 CPU 的方式。

并发服务端的实现方法

下面列出的是具有代表性的并发服务端的实现模型和方法:

  • 多进程服务器:通过创建多个进程提供服务
  • 多路复用服务器:通过捆绑并统一管理 I/O 对象提供服务
  • 多线程服务器:通过生成与客户端等量的线程提供服务

先是第一种方法:多进程服务器,这种方法不适合在 Windows 平台下实现,因为不支持。

理解进程

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

进程是操作系统进行资源分配和调度的一个独立单位,它是程序的运行实例。

假如你下载了一个游戏到电脑上,此时的游戏不是进程,而是程序。只有当游戏被加载到主内存并进入运行状态,这是才可称为进程。

进程ID

无论进程是如何创建的,所有的进程都会被操作系统分配一个 ID。此 ID 被称为进程ID,其值为大于 2 的整数。1 要分配给操作系统启动后的(用于协助操作系统)首个进程,因此用户无法得到 ID 值为 1。

ps 命令可查看当前运行的所有进程,可以通过指定参数 a 和 u 列出所有进程的详细信息:

在这里插入图片描述

通过调用 fork 函数创建进程

创建进程的方式很多,此处只介绍用于创建多进程服务端的 fork 函数。

#include <unistd.h>

pid_t fork(void);

成功时返回进程ID,失败时返回 -1。

fork 函数将创建调用的进程副本。也就是说,并非根据完全不同的程序创建进程,而是复制正在运行的、调用 fork 函数的进程。另外,两个进程都执行 fork 函数调用后的语句(准确的说是在 fork 函数返回后)。但因为是通过同一个进程、复制相同的内存空间,之后的程序流要根据 fork 函数的返回值加以区分。即利用 fork 函数的如下特点区分程序执行流程。

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

此处,父进程(Parent Process)指原进程,即调用 fork 函数的主体,而子进程(Child Process)是通过父进程调用 fork 函数复制出的进程。接下来是调用 fork 函数后的程序运行流程。如图所示:

在这里插入图片描述

复制进程后 gval 与 lval 在两个进程后是独立的,只是共享同一份代码。给出示例程序验证一下:

#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;
}

运行结果:

在这里插入图片描述

可以看出,当执行了 fork 函数之后,此后就相当于有了两个程序在执行代码,对于父进程来说,fork 函数返回的是子进程的ID,对于子进程来说,fork 函数返回 0。所以这两个变量,父进程进行了 +2 操作 ,而子进程进行了 -2 操作,所以结果是这样。

进程和僵尸进程

文件操作中,关闭文件和打开文件同等重要。同样,在进程中,销毁进程和创建进程也同等重要,如果为认真对待进程销毁,将有可能变成僵尸进程。

僵尸进程

进程的工作完成后(执行完 main 函数中的程序后)应被销毁,但有时这些进程将变成僵尸进程,占用系统中的重要资源。这种状态下的进程称作「僵尸进程」,这也是给系统带来负担的原因之一。

僵尸进程是当子进程比父进程先结束,而父进程又没有回收子进程,释放子进程占用的资源,此时子进程将成为一个僵尸进程。如果父进程先退出 ,子进程被init接管,子进程退出后init会回收其占用的相关资源。

产生僵尸进程的原因

终止子进程只有通过下面两种方式:

  • 传递参数并调用 exit 函数
  • main 函数中执行 return 并返回值

exit 中的参数或 main 函数返回值都会传给操作系统,但是并不会销毁进程,直到把这些值传给父进程,处在这样的状态下的进程就是僵尸进程。

操作系统不会主动把这些值传递给父进程。只有父进程主动发起请求(函数调用)的时候,操作系统才会传递该值。换言之,如果父进程未主动要求获得子进程结束状态值,操作系统将一直保存,并让子进程长时间处于僵尸进程状态。

下面代码将创建僵尸进程:

#include <stdio.h>
#include <unistd.h>

int main(int agrc, 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;
}

在程序运行后未结束时,可以查看创建的子进程是否被销毁。

在这里插入图片描述

在这里插入图片描述

在运行程序后将创建进程ID为11616的子进程,通过查看进程状态可以知道PID为11616的进程状态为僵尸进程(Z+)。另外,经过30s后,PID为11615的父进程和之前的僵尸子进程同时销毁。

利用 ./zombie &可以使程序在后台运行,不用打开新的命令行窗口。

销毁僵尸进程 1:利用 wait 函数

为了销毁子进程,父进程需要主动请求获取子进程的结束状态值。共2种发起请求的方法,其中之一就是调用如下函数:

#include <sys/wait.h>
/* Wait for a child to die.  When one does, put its status in *STAT_LOC
   and return its process ID.  For errors, return (pid_t) -1.

   This function is a cancellation point and therefore not marked with
   __THROW.  */
pid_t wait(int *status);

调用此函数时如果已有子进程终止,那么子进程终止时传递的返回值(exit 函数的参数返回值、main 函数的 return 返回值)将保存到该函数的参数所指的内存空间。但函数参数指向的单元中还包含其他信息,因此需要用下列宏进行分离:

  • WIFEXITED(status):检查子进程是否正常退出。如果返回非零值,表示子进程是因调用 exit() 或返回语句结束的。
  • WEXITSTATUS(status):如果子进程是正常退出的(即 WIFEXITED(status) 返回非零值),这个宏会返回子进程的退出代码(exit code),这是由 exit() 函数的参数或 return 语句的值决定的。
  • WIFSIGNALED(status):检查子进程是否是因接收到信号而终止的。如果返回非零值,表示子进程是被信号终止的。
  • WTERMSIG(status):如果子进程是因为信号而终止的(即 WIFSIGNALED(status) 返回非零值),这个宏会返回导致子进程终止的信号。

也就是说,向 wait 函数传递变量 status 的地址时,调用 wait 函数后应编写如下代码:

if (WIFEXITED(status))
{
    puts("Normal termination");
    printf("Child pass num: %d", WEXITSTATUS(status));
}

下面的代码示例将不会产生僵尸进程:

#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;
}

运行结果:

在这里插入图片描述

此时,系统中并没有上述 PID 对应的进程,这是因为调用了 wait 函数,完全销毁了该子进程。另外两个子进程终止时返回的 3 和 7 传递到了父进程。

这就是通过 wait 函数消灭僵尸进程的方法,调用 wait 函数时,如果没有已经终止的子进程,那么程序将阻塞(Blocking)直到有子进程终止,因此要谨慎调用该函数。

销毁僵尸进程 2:使用 waitpid 函数

wait 函数会引起程序阻塞,还可以考虑调用 waitpid 函数。这是防止僵尸进程的第二种方法,也是防止阻塞的方法。

与wait()函数相比,waitpid() 提供了更多的灵活性,因为它允许父进程指定要等待的子进程的 PID 或一组 PID。

#include <sys/wait.h>

pid_t waitpid(pid_t pid, int *status, int options);

成功时返回终止的子进程ID 或 0 ,失败时返回 -1。

参数:

  • pid:等待终止的目标子进程的ID,若传 -1,则与 wait 函数相同,可以等待任意子进程终止
  • status:与 wait 函数的 status 参数具有相同含义
  • options:指定等待操作的选项。常用的选项有 WNOHANG(使 waitpid() 调用非阻塞)、WUNTRACED(使 waitpid() 能够返回停止的子进程)。

示例程序:

#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;
}

运行结果:

在这里插入图片描述

可以看出来,在 while 循环中正好执行了 5 次。这也证明了 waitpid 函数并没有阻塞。

信号处理

如果子进程一直不终止,父进程也不能一直调用waitpid()函数等待子进程终止,因为父进程还有自己的事要做,因此引入信号处理机制,当子进程终止时,让操作系统向父进程发送信号,让父进程立即来处理子进程终止任务。

此处「信号」是在特定事件发生时由操作系统向进程发送的消息。另外,为了响应该消息,执行与消息相关的自定义操作的过程被称为「处理」或「信号处理」。

信号与 signal 函数

下面进程和操作系统的对话可以帮助理解信号处理。

  • 父进程告诉操作系统:如果创建的子进程终止,就帮我调用 zombie_handler 函数
  • 操作系统回复:我可以帮你调用,但是你要先把 zombie_handler 函数编好

相当于父进程需要先注册信号,子进程结束后操作系统直接可以调用。

该请求可以通过如下函数调用完成:

#inlclude <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:子进程终止。

接下来编写调用 signal 函数的语句完成如下请求:「子进程终止则调用 mychild 函数」。

此时 mychild 函数的参数应为 int ,返回值类型应为 void。只有这样才能成为 signal 函数的第二个参数。另外,常数 SIGCHLD 定义了子进程终止的情况,应成为 signal 函数的第一个参数。也就是说,signal 函数调用语句如下:signal(SIGCHLD , mychild);

以上就是信号注册过程。注册好信号之后,发生注册信号时(注册的情况发生时),操作系统将调用该信号对应的函数。先介绍 alarm 函数。

#include <unistd.h>

unsigned int alarm(unsigned int seconds);

返回0或以秒为单位的距 SIGALRM 信号发生所剩时间。

如果调用该函数的同时向它传递一个正整型参数,相应时间后(以秒为单位)将产生 SIGALRM 信号。若向该函数传递为 0 ,则之前对 SIGALRM 信号的预约将取消。如果通过该函数预约信号后未指定该信号对应的处理函数,则(通过调用 signal 函数)终止进程,不做任何处理。

示例程序:

#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;
}

运行结果:

在这里插入图片描述

在第一个等待中我按了CTRL+C,而且在等待过程中实际没有睡眠100s,发生信号时将唤醒由于调用sleep函数而进入阻塞状态的进程。而进程一旦被唤醒,就不会再进入睡眠状态了。

利用 siganction 函数进行信号处理

signal函数在UNIX系列的不同操作系统中可能存在区别,但sigaction函数完全相同。因此,现代 POSIX 标准推荐使用 sigaction() 函数作为替代,下面只讲解其中替代signal()函数的功能。

#include <signal.h>

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

成功时返回 0,失败时返回 -1。

参数:

  • act:对于第一个参数的信号处理函数(信号处理器)信息。
  • oldact:通过此参数获取之前注册的信号处理函数指针,若不需要则传递 0。

sigaction结构体如下:

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

sa_handler成员保存信号处理函数的指针值,sa_mask和sa_flags的所有位均初始化为0即可。可以使用sigemptyset函数将sa_mask成员的所有位初始化为0。

示例程序:

#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;
}

运行结果:

在这里插入图片描述

利用信号处理技术消灭僵尸进程

子进程在终止时会产生SIGCHLD信号,通过这点,利用信号处理机制就能消灭僵尸进程了。

示例程序:

#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);             // 子进程的 pid
        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;
}

运行结果:

在这里插入图片描述

结果中的每一个空行代表间隔了5 秒,程序是先创建了两个子进程,然后子进程 10 秒之后会返回值,第一个 wait 由于子进程在执行,所以直接被唤醒,然后这两个子进程正在睡 10 秒,所以 5 秒之后第二个 wait 开始执行,又过了 5 秒,两个子进程同时被唤醒。所以剩下的 wait 也被唤醒。

所以在本程序的过程中,当子进程终止时候,会向系统发送一个信号,然后调用我们提前写好的处理函数,在处理函数中使用 waitpid 来处理僵尸进程,获取子进程返回值。

基于多任务的并发服务器

基于进程的并发服务器模型

之前的回声服务器每次只能同事向 1 个客户端提供服务。因此,需要扩展回声服务器,使其可以同时向多个客户端提供服务。下图是基于多进程的回声服务器的模型。

在这里插入图片描述

从图中可以看出,每当有客户端请求时(连接请求),回声服务器都创建子进程以提供服务。\为了完成这些任务,需要经过如下过程:

  • 第一阶段:回声服务器端(父进程)通过调用 accept 函数受理连接请求
  • 第二阶段:此时获取的套接字文件描述符创建并传递给子进程
  • 第三阶段:进程利用传递来的文件描述符提供服务

其实不需要把文件描述符传递给子进程,因为子进程有和父进程一样的资源。

实现并发服务器

下面是基于多进程实现的并发的回声服务器的服务端:

#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("Usgae : %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);         // 注册信号处理器,把成功的返回值给 state
    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) // 分配IP地址和端口号
        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, BUFSIZ)) != 0)
                write(clnt_sock, buf, str_len);

            close(clnt_sock);
            puts("client disconnected...");
            return 0;
        }
        else
            close(clnt_sock); // 通过 accept 函数创建的套接字文件描述符已经复制给子进程,因为服务器端要销毁自己拥有的
    }
    close(serv_sock);

    return 0;
}

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

运行结果:

在这里插入图片描述

此时的服务端支持同时给多个客户端进行服务,每有一个客户端连接服务端,就会多开一个子进程,所以可以同时提供服务。

通过 fork 函数复制文件描述符

注意上述代码在子进程运行开始时关闭了服务端套接字,在创建子进程后关闭了客户端套接字。

这是因为调用 fork 函数时赋值父进程的所有资源,但是套接字不是归进程所有的,而是归操作系统所有,只是进程拥有代表相应套接字的文件描述符。这样就有两个文件描述符指向同一个 socket,要关掉多余的文件描述符。

只有当关闭所有的文件描述符才能关闭连接(类似于 shared_ptr)。

在这里插入图片描述

示例中给出了通过 fork 函数复制文件描述符的过程。父进程将 2 个套接字(一个是服务端套接字另一个是客户端套接字)文件描述符复制给了子进程。

如图所示,1 个套接字存在 2 个文件描述符时,只有 2 个文件描述符都终止(销毁)后,才能销毁套接字。如果维持图中的状态,即使子进程销毁了与客户端连接的套接字文件描述符,也无法销毁套接字(服务器套接字同样如此)。因此调用 fork 函数后,要将无关紧要的套接字文件描述符关掉,如图所示:

在这里插入图片描述

分割 TCP 的 I/O 程序

分割 I/O 程序的优点

我们已经实现的回声客户端的数据回声方式如下:向服务器传输数据,并等待服务器端回复。无条件等待,直到接收完服务器端的回声数据后,才能传输下一批数据。

传输数据后要等待服务器端返回的数据,因为程序代码中重复调用了 read 和 write 函数。只能这么写的原因之一是,程序在 1 个进程中运行,现在可以创建多个进程,因此可以分割数据收发过程。默认分割过程如下图所示:

在这里插入图片描述

从图中可以看出,客户端的父进程负责接收数据,额外创建的子进程负责发送数据,分割后,不同进程分别负责输入输出,这样,无论客户端是否从服务器端接收完数据都可以进程传输。

选择这样的实现方式,父进程只需编写接收数据的代码,子进程只需编写发送数据的代码,简化逻辑。

分割 I/O 程序的另外一个好处是,可以提高频繁交换数据的程序性能,如下图所示:

在这里插入图片描述

回声客户端的 I/O 程序分割

下面是回声客户端的 I/O 分割的代码实现:

#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"))
        {
            // 因为fork函数复制了文件描述符,所以通过1次close函数调用传递EOF不够,还需要通过shutdown函数向服务器端另外传递EOF
            shutdown(sock, SHUT_WR);
            return;
        }
        write(sock, buf, strlen(buf));
    }
}

void error_handling(char *message)
{
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}

习题

(1)下列关于进程的说法错误的是?

a. 从操作系统的角度上说,进程是程序运行的单位。
b. 进程根据创建方式建立父子关系。
c. 进程可以包含其他进程,即一个进程的内存空间可以包含其他进程。
d.子进程可以创建其他子进程,而创建出来的子进程还可以创建其子进程,但所有这些进程只与一个父进程建立父子关系。

答:c、d。

(2)调用fork函数将创建子进程,以下关于子进程描述错误的是?

a. 父进程销毁时也会同时销毁子进程。
b. 子进程是复制父进程所有资源创建出的进程。
c. 父子进程共享全局变量。
d. 通过fork函数创建的子进程将执行从开始到fork函数调用为止的代码。

答:a、c、d。

(3)创建子进程时将复制父进程的所有内容,此时的复制对象也包含套接字文件描述符。编写程序验证复制的文件描述符整数值是否与原文件描述符整数值相同。

#include <stdio.h>
#include <unistd.h>
#include <sys/socket.h>
 
int main(int argc, char *argv[])
{
	pid_t pid;
	int sockfd=socket(PF_INET, SOCK_STREAM, 0);
	
	pid=fork();		
	
	if(pid==0)
		printf("Child sock fd: [%d] \n", sockfd);
	else
		printf("Parent sock fd: [%d] \n", sockfd);
	return 0;
}

运行结果:

Parent sock fd: [3] 
Child sock fd: [3] 

(4)请说明进程变为僵尸进程的过程及预防措施​。

僵尸进程是子进程。在子进程结束时,其返回值会传到操作系统,直到返回值被其父进程接收为止前,子进程会一直作为僵尸进程存在。

为了防止这种情况的发生,父进程必须明确接收子进程结束时的返回值。

(5)如果在未注册SIGINT信号的情况下输入CtrI+C,将由操作系统默认的事件处理器终止程序。但如果直接注册CtrI+C信号的处理器,则程序不会终止,而是调用程序员指定的事件处理器。编写注册处理函数的程序,完成如下功能:“输入CtrI+C时,询问是否确定退出程序,输入Y则终止程序”。另外,编写程序使其每隔1秒输出简单字符串,并适用于上述时间处理器注册代码。

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
 
void ctrl_handler(int sig);
 
int main(int argc, char *argv[])
{
	struct sigaction act;
	act.sa_handler=ctrl_handler;
	sigemptyset(&act.sa_mask);
	act.sa_flags=0;
	sigaction(SIGINT, &act, 0);
 
	while(1)
	{
		sleep(1);
		puts("Hello World!");
	}
 
	return 0;
}
 
 
void ctrl_handler(int sig)
{
	char ex;
	fputs("Do you want exit(Y to exit)? ", stdout);
	scanf("%c", &ex);
	if(ex=='y' || ex=='Y')
		exit(1);
}
 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2239124.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

推荐一款DBF文件查看器:DBF Viewer 2000

DBF Viewer 2000是一款强大、简洁且易于使用的 DBF(Clipper、dBase、FoxBase、Foxpro、Visual Foxpro、Visual DBase、VO、DB2K)文件浏览器和编辑器。 软件特点&#xff1a; - 能够快速、有效地浏览和编辑各种类型的 DBF 文件。 - 支持多种文件格式&#xff0c;包括 dBase、V…

基于构件的软件开发、软件维护、区块链技术及湖仓一体架构的应用

目录 试题一 论基于构件的软件开发方法及其应用 试题二 论软件维护方法及其应用 试题三 论区块链技术及应用 试题四 论湖仓一体架构及其应用 相关推荐 试题一 论基于构件的软件开发方法及其应用 基于构件的软件开发(Component-Based Software Development&#xff0c;CBSD…

SCRFD算法解读及环境安装实践

论文地址&#xff1a;https://arxiv.org/abs/2105.04714&#xff0c;accepted by ICLR-2022 工程地址&#xff1a;https://github.com/deepinsight/insightface/tree/master/detection/scrfd 下一篇博客&#xff0c;如何用SCRFD训练自己的数据 文章目录 1、算法解读2、环境安装…

在IntelliJ IDEA中创建带子模块的SpringBoot工程

前言 在项目开发中&#xff0c;一个工程往往有若干子工程或模块&#xff0c;所以主工程一般是一个容器&#xff0c;本文介绍在IntelliJ IDEA中创建带多模块的SpringBoot工程的详细步骤。 1、首先打开IntellJ IDEA&#xff08;以下简称IDEA&#xff09;,创建一个新项目。假定新…

深入剖析输入URL按下回车,浏览器做了什么

DNS 查询 首先,是去寻找页面资源的位置。如果导航到https://example.com, 假设HTML页面被定位到IP地址为93.184.216.34 的服务器。如果以前没有访问过这个网站&#xff0c;就需要进行DNS查询。 浏览器向域名服务器发起DNS查询请求&#xff0c;最终得到一个IP地址。第一次请求…

提升百度排名的有效策略与技巧解析

内容概要 提升百度排名对于网站的成功至关重要。首先&#xff0c;了解百度排名的基本原则&#xff0c;掌握搜索引擎是如何评估网页质量的&#xff0c;是优化过程中不可或缺的一部分。搜索引擎越来越倾向于将用户需求放在首位&#xff0c;因此提供高质量的内容和良好的用户体验…

【GPTs】MJ Prompt Creator:轻松生成创意Midjourney提示词

博客主页&#xff1a; [小ᶻZ࿆] 本文专栏: AIGC | GPTs应用实例 文章目录 &#x1f4af;GPTs指令&#x1f4af;前言&#x1f4af;MJ Prompt Creator主要功能适用场景优点缺点 &#x1f4af; 小结 &#x1f4af;GPTs指令 中文翻译&#xff1a; 任务说明 您是一款为幻灯片工…

机器人领域顶刊TRO十月最新论文一览,覆盖状态估计、任务分配、人机协作等多个领域

No.1 基于可见性的近似追逃方法 论文标题&#xff1a;Approximate Methods for Visibility-Based Pursuit–Evasion 中文标题&#xff1a;基于可见性的近似追逃方法 作者&#xff1a;Emmanuel Antonio; Israel Becerra; Rafael Murrieta-Cid 本文提出了一种基于采样的动态规…

解决编译 fast-lio-lc 算法时遇到的error方法

1.创建工作空间和下载 fast-lio-lc功能包 mkdir -p fast_lio_lc_ws/src cd fast_lio_lc_ws/src/ catkin_init_workspace git clone https://github.com/yanliang-wang/FAST_LIO_LC.git2.进入工作空间,编译 编译 fast-lio-lc遇到的error: 🕐error: fatal error: opencv/cv…

【Qt】Macbook M1下载安装

文章目录 一、下载Xcode命令行工具二、在Cion中配置编译器三、安装Qt四、配置qmake环境五、创建Qt项目 博主已经下载了Clion&#xff0c;所以本文是将qt配置到Clion上 本博客所写的教程有一定的问题&#xff0c;因为我在官网下载后发现有一些所需的包是没有的&#xff0c;不知道…

Python+Pytest+Allure+Git+Jenkins接口自动化框架

一、接口基础 接口测试是对系统和组件之间的接口进行测试&#xff0c;主要是效验数据的交换&#xff0c;传递和控制管理过程&#xff0c;以及相互逻辑依赖关系。其中接口协议分为HTTP&#xff0c;RPC&#xff0c;Webservice&#xff0c;Dubbo&#xff0c;RESTful等类型。 接口…

Docker:镜像构建 DockerFile

Docker&#xff1a;镜像构建 DockerFile 镜像构建docker build DockerfileFROMCOPYENVWORKDIRADDRUNCMDENTRYPOINTUSERARGVOLUME 镜像构建 在Docker官方提供的镜像中&#xff0c;大部分都是基础镜像&#xff0c;他们只提供某个简单的功能&#xff0c;如果想要一个功能更加丰富…

《JavaEE进阶》----20.<基于Spring图书管理系统①(登录+添加图书)>

PS&#xff1a;关于接口定义 接口定义&#xff0c;通常由服务器提供方来定义。 1.路径&#xff1a;自己定义 2.参数&#xff1a;根据需求考虑&#xff0c;我们这个接口功能完成需要哪些信息。 3.返回结果&#xff1a;考虑我们能为对方提供什么。站在对方角度考虑。 我们使用到的…

【JavaEE】文件io

目录 文件类型 File概述 属性 构造方法 常用方法 Reader Writer InputStream OutputStream 字节流转字符流 通过Scanner读取InputStream 通过PrintWriter转换outputstream 示例 文件类型 从编程的角度看&#xff0c;文件类型主要就是两大类 文本&#xff08;文…

D3入门:概念、主要特点、基本功能、常见应用场景

D3.js&#xff08;Data-Driven Documents&#xff09;是一个JavaScript库&#xff0c;用于基于数据操作文档。它利用了HTML、SVG和CSS等Web标准技术&#xff0c;使得开发者可以创建丰富的交互式图表和数据可视化。D3.js的强大之处在于其灵活的数据绑定机制和对DOM元素的高效操作…

go函数传值是值传递?还是引用传递?slice案例加图解

先说下结论 Go语言中所有的传参都是值传递&#xff08;传值&#xff09;&#xff0c;都是一个副本&#xff0c;一个拷贝。 值语义类型&#xff1a;参数传递的时候&#xff0c;就是值拷贝&#xff0c;这样就在函数中就无法修改原内容数据。 基本类型&#xff1a;byte、int、bool…

tensorflow案例5--基于改进VGG16模型的马铃薯识别,准确率提升0.6%,计算量降低78.07%

&#x1f368; 本文为&#x1f517;365天深度学习训练营 中的学习记录博客&#x1f356; 原作者&#xff1a;K同学啊 前言 本次采用VGG16模型进行预测&#xff0c;准确率达到了98.875&#xff0c;但是修改VGG16网络结构&#xff0c; 准确率达到了0.9969&#xff0c;并且计算量…

攻防世界38-FlatScience-CTFWeb

攻防世界38-FlatScience-Web 点开这个here看到一堆pdf,感觉没用&#xff0c;扫描一下 试试弱口令先 源码里有&#xff1a; 好吧0.0 试试存不存在sql注入 根本没回显&#xff0c;转战login.php先 输入1’,发现sql注入 看到提示 访问后得源码 <?php ob_start(); ?>…

数据分析-44-时间序列预测之深度学习方法TCN

文章目录 1 TCN简介1.1 网络示意图1.2 TCN优点2 模拟应用2.1 模拟数据2.2 预处理创建滞后特征2.3 划分训练集和测试集2.4 创建TCN模型2.5 模型训练2.6 模型预测3 自定义my_TCN模型3.1 my_TCN()函数3.2 训练模型3.3 模型预测3.4 改进4 参考附录1 TCN简介 时间卷积网络(TCN)是…

C++【STL容器系列(二)】vector的模拟实现

文章目录 1. vector的结构2. vector的默认成员函数2.1构造函数2.1.1 默认构造2.1.2 迭代器构造2.1.3 用n个val初始化构造 2.2 拷贝构造2.3 析构函数2.4 operator 3. vector iterator函数3.1 begin 和 cbegin函数3.2 end() 和 cend()函数 4. vector的小函数4.1 size函数4.2 capa…