10~14章

十.多进程编程

1.创建进程

#include<unistd.h>

//成功0,失败-1
pid_t fork(void);
//父进程返回子进程ID,子进程返回0

2.僵尸进程

子进程有两种结束方式

1.调用exit函数并传递参数
	exit(1);
2.main函数中执行return语句返回值
	return 0;

这两种结束方式传递的参数都会传递给OS,此时OS不会销毁子进程,直到把这些值传递给了创建该子进程的父进程,或者父进程被销毁,在传递成功之前进程就是僵尸进程。

销毁僵尸进程的方法就是父进程要主动获取子进程的结束状态值

2.1 销毁僵尸进程

方法1. wait() - 阻塞

#include<sys/wait.h>

/*
	任意一个子进程结束时,函数返回
	成功时返回子进程ID,失败-1
	子进程终止时的信息存在statloc中
*/ 
pid_t wait(int* statloc);

statloc中除了返回值还包含很多其他信息,可使用宏进行读取

// 子进程正常终止,以下宏返回true
WIFEXITED(statloc);
// 返回子进程的返回值
WEXITSTATUS(statloc);

方法2: waitpid() - 非阻塞

#include<sys.wait.h>

/*
	成功时返回子进程ID(或0),失败-1
	1.pid: 等待的目标子进程的ID,若传递-1,则与wait函数相同,等待任意子进程终止
	2.statloc: 与wait函数的statloc含义相同
	3.options: 传递头文件sys/wait.h中声明的常量WNOHANG,即使没有终止的子进程也不会阻塞,而是返回0,并退出函数。
*/ 
pid_t waitpid(pid_t pid, int* statloc, int options);

3.信号处理

上两种方法需要父进程经常查看子进程状态并处理,太浪费资源,因此产生一种新方法 - 信号,当子进程结束时,主动通知某个函数进行处理,从而解放父进程

3.1 signal()

#include<signal.h>
/*
	1.signo: 发生alarm的信息
	2.第二个参数是发生情况时要调用的handler
*/
void (*signal(int signo, void (*func)(int)))(int);

这里给出signo的部分选项

SIGALRM:	alarm到期
SIGINT: 	输入CTRL+C
SIGCHILD: 子进程终止

alarm()

#include<unistd.h>

// 返回0或者以秒为单位的距alarm发生所剩时间
// 如果当前进程没有注册alarm handler,该进程会被终止
unsigned int alarm(unsigned int seconds);

即便进程处于sleep 阻塞状态,也会被signal唤醒,而且唤醒后不会再继续sleep

#include <cstdio>
#include <unistd.h>
#include <csignal>

void timeout(int sig){
    printf("Time Out\n");
    alarm(2);
}

void keycontrol(int sig){
    printf("CTRL+C pressed.\n");
}

int main(){
  	//当出现alarm到期时,调用timeout
    signal(SIGALRM, timeout);
  	//当出现输出CTRL+C时,调用keycontrol
    signal(SIGINT, keycontrol);
    alarm(2);
    for(int i = 0; i < 3; i++)
    {
        printf("Wait...\n");
        sleep(100);
    }
}

3.2 sigaction()

实际上很少使用signal(),一般都是sigaction()

#include<signal.h>
/*
	成功0,失败-1
	1.signo: 需要响应的信号
	2.act: 传递handler
	3.oldact: 获取此信号信息之前注册的handler,若不需要则传递0
*/
int sigaction(int signo, const struct sigaction* act, struct sigaction* oldact);

//sa_hanlder就是handler,sa_mask和sa_flags目前置0即可
struct sigaction{
  void (*sa_hanlder)(int);
  sigset_t sa_mask;
  int sa_flags;
}
struct sigaction handler{};
handler.sa_handler = timeout;
sigemptyset(&handler.sa_mask);
handler.sa_flags = 0;
sigaction(SIGALRM, &handler, nullptr);
alarm(2);

4.并发服务器

处理逻辑如下

  1. 父进程通过accpet接收连接请求
  2. 通过accept()创建socket并传递给子进程
  3. 子进程利用socket发送消息

在fork时,子进程会获取到父进程的所有socket(其实父子进程拥有的并不是socket,而是socket对应的文件描述符,类似于指向socket的引用,因为socket是系统资源,并不为进程所拥有,)。

因此此时server和client socket都会有两个引用,因此,在fork完毕后,父进程需要关闭client_sock,而子进程需要关闭serv_sock。只有当父子进程的socket都关闭时,socket才会销毁

image-20221201131804212

5. 分割TCP I/O的程序

之前实现的echo客户端,工作流程如下:

  1. 向服务器端发送数据
  2. 等待回复
  3. 等到回声数据后,再发送下一批数据

可以对客户端进行优化,创建两个进程,分割数据收发过程:

父进程负责接收,子进程负责发送

分割I/O可以提高交换数据的程序性能。下面是传统和多进程发送的对比

image-20221202002842785

十一.进程间通信

使用单个管道进行进程间通信

#include<unistd.h>

/*
	成功0, 失败-1
	fd[0],管道出口,读数据
	fd[1],管道入口,写数据
*/
int pipe(int fd[2]);
image-20221202154012575

使用两个管道完成进程间双向通信

image-20221202154140811

十二.I/O复用

一个进程只监视一个端口效率太低,考虑到网络传输和CPU速度的巨大差距,进程大部分时间都是闲置的,因此有了IO复用,一次监视多个端口

1.select函数

image-20230716131154677
#include<sys/select.h>
#include<sys/time.h>

/*
	发生错误返回-1,超时返回0
	发生事件时返回大于0的值,表示发生事件的文件描述符数目
	1.maxfd: 监视到的文件描述符的最大值
	2.readset   关注"是否存在待读取数据"的文件描述符
	3.writeset  关注“是否可传输无阻塞数据”的文件描述符
	4.exceptset 关注 “是否发生异常"的文件描述符
	5.timeout: 调用select函数后,为防止陷入无限阻塞的状态,传递超时(time-out)信息
*/ 
int select(int maxfd, fd_set* readset, fd_set* writeset, fd_set* exceptset, const struct timeval* timeout);

事件:监视的socket来了数据就叫发生了事件

文件描述符注册

fd_set fdset;

FD_ZERO(fd_set* fdset); //将fd_set变量的所有位初始化为0
FD_SET(int fd, fd_set* fdset); //在fdset中注册fd
FD_CLR(int fd, fd_set* fdset); //从fdset中清楚fd的信息
FD_ISSET(int fd, fd_set* fdset); //判断fdset中是否包含fd

fd_set是一个包含了所有文件描述符的数组,如果进行了注册,对应位置会变为1

image-20221202163800220

设置超时时间

struct timeval{
  long tv_sec; //seconds
  long tv_usec; //mocirseconds
}

2.查看select调用结果

select调用完成后,fd_set的值会发生变化,原来为1的位会变为0,但如果对应位置的文件描述符发生了事件,那么他会保持为1,可通过这个特性判断是否有事件发生。

十三.多种I/O函数

1.send & recv

#include<sys.socket.h>

//成功返回发送的字节数,失败-1
ssize_t send(int sockfd, const void* buf, size_t nbytes, int flags);

//成功返回接收字节数(收到EOF时返回0),失败-1
ssize_t recv(int sockfd, void* buf, size_t nbytes, int flags);

可选flags

选项 含义 send recv
MSG_OOB 用于传输带外数据
MSG_PEEK 验证输入缓冲中是否存在接收的数据
MSG_DONTROUTE 数据传输过程中不参照路由表,在本地网络中寻找目的地
MSG_DONTWAIT 调用IO函数时不阻塞,用于使用非阻塞IO
MSG_WAITALL 防止函数返回,直到接收全部请求的字节数

MSG_OOB

发送紧急消息

image-20221204153955553

就是设置了URG和紧急指针,加快处理,需要注意,使用紧急消息并不会加快传输速度,只会加快处理速度

MSG_PEEK & MSG_DONTWAIT

使用MSG_PEEK会从输入缓冲读取数据,但是读取完后,不会删除输入缓冲中已读取数据,

使用MSG_DONTWAIT进行接收时,不会阻塞,因此这两个通常配合使用,检查有没有要处理的数据

while(1)
{
  // recv不会阻塞
  int str_len = recv(sock, buf, sizeof(buf)-1, MSG_PEEK|MSG_DONTWAIT);
  if(str_len > 0) break;
}
//这里还能读取到之前的消息
int str_len = recv(sock, buf, sizeof(buf)-1, 0);

2.readv & writev

对较小数据包进行整合,一次发送出去,提高通信效率

#include<sys/uio.h>

struct iovec{
  void* iov_base; // 缓冲地址
  size_t iov_len;	// 缓冲大小
}

/*
	成功时返回发送字节数,失败-1
	1.fds: 数据传输对象的套接字文件描述符
	2.iov: iovec结构体数组的地址值,结构体中包含待发送数据的位置和大小
	3.iovcnt: iovec结构体数组长度
*/
ssize_t writev(int fds, const struct iovec* iov, int iovcnt);

/*
	成功返回接收字节数,失败-1
	1.fds: 传递接收数据的套接字
	2.iov: 包含可保存数据的位置和大小信息
	3.iovcnt: 第二个参数的长度
*/
ssize_t readv(int filedes, const struct iovec* iov, int iovcnt);

demo

#include <stdio.h>
#include <sys/uio.h>
#include <string.h>

int main(){
    struct iovec vec[2];
    char buf1[] = "ABCDEFG";
    char buf2[] = "123456";
    vec[0].iov_base = buf1;
    vec[0].iov_len = strlen(buf1);
    vec[1].iov_base = buf2;
    vec[1].iov_len = strlen(buf2);

    ssize_t strlen = writev(0, vec, 2);
    puts("");
    printf("Write Bytes: %zd.\n", strlen);

  	// read, 先往buf1放,然后往buf2放数据
    strlen = readv(0, vec, 2);
    printf("%s, %s", buf1, buf2);
    puts("");
    printf("Read Bytes: %zd.\n", strlen);
    return 0;
}

十四.多播和广播

posted @ 2024-07-03 00:11  INnoVation-V2  阅读(3)  评论(0编辑  收藏  举报