15~18.基于Linux编程
十五.套接字和标准IO
1.标准IO函数
标准函数的优点
-
具有良好的移植性
-
标准IO会提供缓冲,可提高性能
标准函数的缺点
- 不容易进行双向通信
- 有时会频繁调用fflush函数
- 需要传入FILE指针
2.使用标准IO函数
fd
-->FILE*
#includes<stdio.h>
/*
失败返回NULL
1.fd: 文件描述符
2.mode: FILE指针的创建模式
*/
FILE* fdopen(int fd, const char* mode);
常见的打开模式
模式参数 | 描述 |
---|---|
"r" | 以只读方式打开文件。文件必须存在,否则打开失败。 |
"w" | 以写入方式打开文件。如果文件存在,则会截断文件为空;如果文件不存在,则会创建新文件。 |
"a" | 以追加方式打开文件。如果文件存在,则将数据追加到文件末尾;如果文件不存在,则会创建新文件。 |
"r+" | 以读写方式打开文件。文件必须存在,否则打开失败。 |
"w+" | 以读写方式打开文件。如果文件存在,则会截断文件为空;如果文件不存在,则会创建新文件。 |
"a+" | 以追加方式打开文件。如果文件存在,则将数据追加到文件末尾;如果文件不存在,则会创建新文件。 |
"b" | 以二进制模式打开文件。可与其他模式参数组合使用。 |
"t" | 以文本模式打开文件(默认模式)。可与其他模式参数组合使用。 |
FILE*
-->fd
#includes<stdio.h>
// 失败返回-1
int fileno(FILE* stream);
附:常见标准IO函数
文件打开和关闭
函数名称 | 描述 |
---|---|
fopen() | 打开文件 |
fclose() | 关闭文件 |
字符输入输出
函数名称 | 描述 |
---|---|
fgetc() | 从文件中读取一个字符 |
fgets() | 从文件中读取一行字符串 |
fputc() | 将一个字符写入文件 |
fputs() | 将一个字符串写入文件 |
fprintf() | 格式化写入文件 |
fscanf() | 格式化读取文件 |
二进制数据读写
函数名称 | 描述 |
---|---|
fread() | 从文件中读取二进制数据块 |
fwrite() | 将二进制数据块写入文件 |
定位和移动文件指针
函数名称 | 描述 |
---|---|
fseek() | 定位文件指针的位置 |
ftell() | 获取文件指针的当前位置 |
rewind() | 将文件指针重置到文件开头 |
文件操作状态查询
函数名称 | 描述 |
---|---|
feof() | 检查文件结束标志 |
ferror() | 检查文件错误标志 |
文件流刷新和刷新控制
函数名称 | 描述 |
---|---|
fflush() | 刷新文件流 |
setbuf() | 设置文件流的缓冲区 |
setvbuf() | 设置文件流的缓冲区和类型 |
标准错误流操作
函数名称 | 描述 |
---|---|
perror() | 输出上一个函数调用的错误信息 |
strerror() | 获取错误代码对应的字符串描述 |
十六.分离IO流
第7章用fork+半关闭
分离IO流,让一个进程负责输入,一个进程负责输出
第15章提供了另一个方法,就是利用fdopen()
创建出两个FILE指针,一个r
模式,一个w
模式,达到分离IO流的作用
但是存在一个问题:w
流在写入完成后,不能主动关闭,因为会同时关闭r
流,原因是两个FILE指向的是同一个socket,如下所示

关闭其中一个,也会导致文件描述符被销毁,然后socket也被销毁。

解决办法是,复制出两个文件描述符,指向同一个socket,关闭一个流,也只会销毁一个,不影响socket和另一个文件描述符。

复制文件描述符
#include <unistd.h>
/*
成功时返回复制的文件描述符,失败时返回-1
1.dup()是复制文件描述符,复制fd到随机的某个文件描述符并返回
2.dup2()是指定复制的文件描述符,复制fd到targetFd
*/
int dup(int fd);
int dup2(int fd, int targetFd);
十七.epoll
epoll涉及三个函数
epoll_create
: 创建保存epoll文件描述符的空间
epoll_ctl
: 注册或注销文件描述符
epoll_wait
: 与select类似,等待文件描述符发生变化
1.epoll_create
#include <sys/epoll.h>
//成功时返回epoll文件描述符,失败-1
int epoll_create();
2.epoll_ctl
#include <sys/epoll.h>
/*
成功时返回epoll文件描述符,失败-1
1.epfd: 要注册的epoll文件描述符
2.op: 要执行的操作。增删改?
3.fd: 要注册的文件描述符
4.event: 监视对象的事件类型
*/
int epoll_ctl(int epfd, int op, int fd, struct epoll_event* event);
第二个参数常见选项
EPOLL_CTL_ADD //将文件描述符注册到epoll
EPOLL_CTL_DEL
EPOLL_CTL_MOD //修改
第四个参数,epoll_event
,用于描述一个事件
struct epoll_event {
__uint32_t events; // 表示注册的事件类型
epoll_data_t data; // 用户数据,可以是文件描述符或指针等
};
typedef union epoll_data {
void *ptr; // 用于指针类型数据
int fd; // 用于文件描述符类型数据
__uint32_t u32; // 用于 32 位无符号整数类型数据
__uint64_t u64; // 用于 64 位无符号整数类型数据
} epoll_data_t;
events
选项EPOLLIN
: 文件描述符上有数据可读。EPOLLOUT
:文件描述符上有数据可写。EPOLLERR
:文件描述符上发生错误。EPOLLHUP
:文件描述符上发生挂起事件。EPOLLET
: 使用边缘触发模式(Edge Triggered)。EPOLLONESHOT
:注册一次性事件,只触发一次后自动失效。EPOLLRDHUP
: 断开连接或半关闭的情况
data
:表示与事件关联的用户数据。它是一个联合体类型epoll_data_t
,可以是文件描述符、指针或者其他用户自定义的数据类型,具体取决于注册事件时的设置。
3.epoll_wait
#include <sys/epoll.h>
/*
成功时返回就绪事件的数量,失败返回 -1
1. epfd: epoll的文件描述符
2. events: 接收就绪事件的数组
3. maxevents: events数组的大小,即最多能够接收多少个就绪事件
4. timeout: 等待就绪事件的超时时间,以毫秒为单位。传递-1表示无限等待,0表示非阻塞调用,传递正整数表示等待的时间限制
*/
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
4.条件触发和边缘触发
条件触发(Level-Triggered):
- 默认的触发方式。
- 当IO缓冲区有数据可读或可写时,条件触发会一直通知监听者,直到所有数据被读取或写入完成。
边缘触发(Edge-Triggered):
- 当事件状态发生变化时(例如,缓冲区由空变为非空或由非空变为空),系统会通知,但是只会在事件状态变化时通知一次。
- 边缘触发只会通知一次,需要应用程序自行处理数据。
5. 条件触发测试
使用默认的条件触发,发送5个字节,每次只读取1字节,看看会通知多少次
代码地址
6. 实现边缘触发
要实现边缘触发,有两个知识需要了解
- 通过errno变量验证错误原因
- 完成非阻塞IO,更改socket特性
errno
声明在error.h
中,int类型,可通过这个值获取错误原因,
和本章有关的是,read函数发现输入缓冲中没有数据可读时返回-1,同时在errno中保存EAGAIN
常量
边缘触发和error有什么关系呢?
边缘触发后,接收数据只会注册1次事件,因此一旦收到数据,就应读取所有数据,因此就需要验证输入缓冲是否为空,通过errno就可以确定这一点
更改套接字为非阻塞
#include <fcntl.h>
//file control
int fcntl(int fd, int cmd);
DEMO
int flag = fcntl(fd, F_GETFL, 0);
fcntl(fd, F_SETFL, flag|O_NONBLOCK);
使用非阻塞IO
- 套接字在执行读写时不会阻塞。如果没有数据可读或无法写入,将立即返回一个错误码,来指示当前没有可用的数据或无法立即写入。
- 当没有可读的数据时,非阻塞套接字的读取将返回 0,而不是阻塞等待到达。
为什么要用非阻塞,因为阻塞模式下read和write会导致长时间停顿,所以要用非阻塞
7. 实现边缘触发的echo server
代码
8.边缘触发的和条件触发的对比
边缘触发和条件触发相比,优势不在于速度,从实现模型的角度看,边缘触发更有可能带来高性能,但不能简单地认为 “只要使用边缘触发就一定能提高速度”。
边缘触发的优势在于可以选择处理数据的时机,即接收到数据后可以选择过会再处理。
如果是条件触发,如果不处理,则每次调用epoll_wait()
时都会产生事件。事件数会累加,服务器端将不能承受。
十八.多线程服务器端的实现
在 Linux 内核中,线程调度相对于进程调度来说,节省了以下几个资源:
- 上下文切换开销:线程的上下文切换开销通常比进程的上下文切换开销要小。由于线程共享同一进程的地址空间,线程切换时只需切换线程的堆栈和寄存器等少量上下文信息。相比之下,进程切换需要保存和恢复更多的上下文信息,包括内存映像、文件描述符表和其他进程相关的资源。
- 内存开销:线程共享同一进程的地址空间,不需要为每个线程分配独立的内存空间。相比之下,进程需要为每个进程分配独立的内存空间,包括代码段、数据段、堆栈等,这会导致更大的内存开销。
- 创建和销毁开销:线程的创建和销毁通常比进程的创建和销毁开销要小。线程的创建只需要为其分配堆栈空间和一些管理数据结构,而进程的创建需要复制父进程的资源、建立新的地址空间等操作。
- 调度开销:线程的调度相对于进程来说更加轻量级。调度器只需管理线程的上下文切换和调度,而进程调度需要涉及更多的资源管理和调度算法。
- 通信开销:线程之间共享相同的进程上下文,因此线程间通信的成本较低。它们可以通过共享内存、全局变量等直接进行通信,而进程间通信则需要通过更复杂的机制,如管道、消息队列、共享内存等,增加了通信的开销和复杂性。
需要注意的是,尽管线程调度相对节省一些资源,但也需要注意线程之间的竞争和同步问题,确保线程安全性。此外,线程的并发访问共享资源可能引发竞争和同步开销,需要使用适当的同步机制来保证线程安全性。因此,在设计和开发多线程应用程序时,仍需要综合考虑资源的合理管理和线程间的协同工作。
1. 线程的创建
#include<pthread.h>
/*
成功返回0, 失败-1
1. thread:指向pthread_t类型的指针,存储新创建的线程的标识符。
1. attr:指向pthread_attr_t类型的指针,用于设置线程的属性,通常可以设置为NULL,表示使用默认属性。
3. start_routine:指向新线程Main函数的指针
4. arg:要传递的参数
*/
int pthread_create(pthread_t restrict thread,
const pthread_attr_t * restrict attr,
void * (* start_routine)(void *),
void * restrict arg);
Example:
CH-18/thread1.c
当主进程结束时,线程会被摧毁
2. 等待线程执行完成
#include<pthread.h>
/*
成功0,失败-1
thread: 要等待的thread
status: 线程main函数的返回值的地址
*/
int pthread_join(pthread_t thread, void **status);
Example:
CH-18/thread2.c

pthread_join
会等待指定线程执行完成,然后继续执行
线程安全的函数
线程运行时,需要考虑是否会引起临界区问题,按照是否会引起,分为两类函数
- 线程安全函数
- 非线程安全函数
可通过编译时添加-D_REENTRANT
选项,将非线程安全函数替换为线程安全函数
3. 临界区问题
Example:
CH-18/thread3.c
上述代码中,因为临界区问题,结果会出错,但是要注意,全局变量sum本身并不是临界区,临界区是指引起问题的代码块,所以sum += 1
和sum-=1
才是临界区
4. 解决临界区问题
mutex互斥访问
#include <pthread.h>
/*
成功0,失败返回其他值
mutex: 保存互斥量的变量的地址值
attr: 要创建的互斥量的属性,没有要指定的属性时传递NULL
*/
int pthread_mutex_init(
pthread_mutex_t* mutex,
const pthread_mutexattr_t* attr);
int pthread_mutex_destory(pthread_mutex_t* mutex);
int pthread_mutex_lock(pthread_mutex_t* mutex);
int pthread_mutex_unlock(pthread_mutex_t* mutex);
Example:
CH-18/mutex.c
信号量
#include <semaphore.h>
/*
成功0,失败返回其他值
sem:指向 sem_t 类型的指针,用于指定要初始化的信号量。
pshared:用于指定信号量的共享类型。可以设置为 0(默认值)表示信号量是进程内共享的,或者设置为 1 表示信号量是进程间共享的(需要支持进程间通信的系统)。
value:信号量的初始值。指定信号量的初始资源数量。
*/
int sem_init(sem_t *sem, int pshared, unsigned int value);
int sem_destroy(sem_t *sem);
//post加1,wait减1
int sem_post(sem_t *sem);
int sem_wait(sem_t *sem);
Example:
CH-18/semaphore.c
5. 线程的销毁
#include <pthread.h>
int pthread_join(pthread_t thread, void **status);
int pthread_detach(pthread_t thread);
这两个函数都可以销毁线程,但是join会阻塞当前线程,因此可以使用pthread_detach
, pthread_detach
函数不会终止指定的线程。它只是将线程标记为分离状态,使得线程在终止时能够自动释放资源,无需其他线程调用 pthread_join
来回收。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· 百万级群聊的设计实践
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战
· 永远不要相信用户的输入:从 SQL 注入攻防看输入验证的重要性
2022-07-03 ccache安装配置
2022-07-03 配置SSH
2021-07-03 Type interface com.innovationV2.mapper.UserMapper is not known to the MapperRegistry