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.并发服务器
处理逻辑如下
- 父进程通过accpet接收连接请求
- 通过
accept()
创建socket并传递给子进程 - 子进程利用socket发送消息
在fork时,子进程会获取到父进程的所有socket(其实父子进程拥有的并不是socket,而是socket对应的文件描述符,类似于指向socket的引用,因为socket是系统资源,并不为进程所拥有,)。
因此此时server和client socket
都会有两个引用,因此,在fork完毕后,父进程需要关闭client_sock
,而子进程需要关闭serv_sock
。只有当父子进程的socket都关闭时,socket才会销毁
![image-20221201131804212](http://pic-save-fury.oss-cn-shanghai.aliyuncs.com/uPic/image-20221201131804212.png)
5. 分割TCP I/O的程序
之前实现的echo客户端,工作流程如下:
- 向服务器端发送数据
- 等待回复
- 等到回声数据后,再发送下一批数据
可以对客户端进行优化,创建两个进程,分割数据收发过程:
父进程负责接收,子进程负责发送
分割I/O可以提高交换数据的程序性能。下面是传统和多进程发送的对比
![image-20221202002842785](http://pic-save-fury.oss-cn-shanghai.aliyuncs.com/uPic/image-20221202002842785.png)
十一.进程间通信
使用单个管道进行进程间通信
#include<unistd.h>
/*
成功0, 失败-1
fd[0],管道出口,读数据
fd[1],管道入口,写数据
*/
int pipe(int fd[2]);
![image-20221202154012575](http://pic-save-fury.oss-cn-shanghai.aliyuncs.com/uPic/image-20221202154012575.png)
使用两个管道完成进程间双向通信
![image-20221202154140811](http://pic-save-fury.oss-cn-shanghai.aliyuncs.com/uPic/image-20221202154140811.png)
十二.I/O复用
一个进程只监视一个端口效率太低,考虑到网络传输和CPU速度的巨大差距,进程大部分时间都是闲置的,因此有了IO复用,一次监视多个端口
1.select函数
![image-20230716131154677](http://pic-save-fury.oss-cn-shanghai.aliyuncs.com/uPic/image-20230716131154677.png)
#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](http://pic-save-fury.oss-cn-shanghai.aliyuncs.com/uPic/image-20221202163800220.png)
设置超时时间
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](http://pic-save-fury.oss-cn-shanghai.aliyuncs.com/uPic/image-20221204153955553.png)
就是设置了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;
}
十四.多播和广播
略