15~18.基于Linux编程

十五.套接字和标准IO

1.标准IO函数

标准函数的优点

  • 具有良好的移植性

  • 标准IO会提供缓冲,可提高性能

    image-20230716161554668

标准函数的缺点

  • 不容易进行双向通信
  • 有时会频繁调用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,如下所示

image-20230716195540765

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

image-20230716195625478

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

image-20230716195822015

复制文件描述符

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

  1. events选项
    • EPOLLIN: 文件描述符上有数据可读。
    • EPOLLOUT:文件描述符上有数据可写。
    • EPOLLERR:文件描述符上发生错误。
    • EPOLLHUP:文件描述符上发生挂起事件。
    • EPOLLET: 使用边缘触发模式(Edge Triggered)。
    • EPOLLONESHOT:注册一次性事件,只触发一次后自动失效。
    • EPOLLRDHUP: 断开连接或半关闭的情况
  2. 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

  1. 套接字在执行读写时不会阻塞。如果没有数据可读或无法写入,将立即返回一个错误码,来指示当前没有可用的数据或无法立即写入。
  2. 当没有可读的数据时,非阻塞套接字的读取将返回 0,而不是阻塞等待到达。

为什么要用非阻塞,因为阻塞模式下read和write会导致长时间停顿,所以要用非阻塞

7. 实现边缘触发的echo server

代码

8.边缘触发的和条件触发的对比

边缘触发和条件触发相比,优势不在于速度,从实现模型的角度看,边缘触发更有可能带来高性能,但不能简单地认为 “只要使用边缘触发就一定能提高速度”。

边缘触发的优势在于可以选择处理数据的时机,即接收到数据后可以选择过会再处理。

如果是条件触发,如果不处理,则每次调用epoll_wait()时都会产生事件。事件数会累加,服务器端将不能承受。

十八.多线程服务器端的实现

在 Linux 内核中,线程调度相对于进程调度来说,节省了以下几个资源:

  1. 上下文切换开销:线程的上下文切换开销通常比进程的上下文切换开销要小。由于线程共享同一进程的地址空间,线程切换时只需切换线程的堆栈和寄存器等少量上下文信息。相比之下,进程切换需要保存和恢复更多的上下文信息,包括内存映像、文件描述符表和其他进程相关的资源。
  2. 内存开销:线程共享同一进程的地址空间,不需要为每个线程分配独立的内存空间。相比之下,进程需要为每个进程分配独立的内存空间,包括代码段、数据段、堆栈等,这会导致更大的内存开销。
  3. 创建和销毁开销:线程的创建和销毁通常比进程的创建和销毁开销要小。线程的创建只需要为其分配堆栈空间和一些管理数据结构,而进程的创建需要复制父进程的资源、建立新的地址空间等操作。
  4. 调度开销:线程的调度相对于进程来说更加轻量级。调度器只需管理线程的上下文切换和调度,而进程调度需要涉及更多的资源管理和调度算法。
  5. 通信开销:线程之间共享相同的进程上下文,因此线程间通信的成本较低。它们可以通过共享内存、全局变量等直接进行通信,而进程间通信则需要通过更复杂的机制,如管道、消息队列、共享内存等,增加了通信的开销和复杂性。

需要注意的是,尽管线程调度相对节省一些资源,但也需要注意线程之间的竞争和同步问题,确保线程安全性。此外,线程的并发访问共享资源可能引发竞争和同步开销,需要使用适当的同步机制来保证线程安全性。因此,在设计和开发多线程应用程序时,仍需要综合考虑资源的合理管理和线程间的协同工作。

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

image-20230724153925585

pthread_join会等待指定线程执行完成,然后继续执行

线程安全的函数

线程运行时,需要考虑是否会引起临界区问题,按照是否会引起,分为两类函数

  1. 线程安全函数
  2. 非线程安全函数

可通过编译时添加-D_REENTRANT选项,将非线程安全函数替换为线程安全函数

3. 临界区问题

Example:CH-18/thread3.c

上述代码中,因为临界区问题,结果会出错,但是要注意,全局变量sum本身并不是临界区,临界区是指引起问题的代码块,所以sum += 1sum-=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_detachpthread_detach 函数不会终止指定的线程。它只是将线程标记为分离状态,使得线程在终止时能够自动释放资源,无需其他线程调用 pthread_join 来回收。

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