目录
C++11 中std::function和std::bind的用法
这篇博客是我在学习muduo时,参考他动手写多线程服务器端模型的笔记,果然自己多动手写代码才能学到更多东西呀。。
eventfd 介绍
https://cloud.tencent.com/developer/article/1160842
Linux 2.6.27后添加了一个新的特性,就是eventfd,是用来实现多进程或多线程的之间的事件通知的。
eventfd是一个用来通知事件的文件描述符,timerfd是的定时器事件的文件描述符。二者都是内核向用户空间的应用发送通知的机制,可以有效地被用来实现用户空间的事件/通知驱动的应用程序。
(对于timerfd,还有精准度和实现复杂度的巨大差异。由内核管理的timerfd底层是内核中的hrtimer(高精度时钟定时器),可以精确至纳秒(1e-9秒)级,完全胜任实时任务。而用户态要想实现一个传统的定时器,通常是基于优先队列/二叉堆,不仅实现复杂维护成本高,而且运行时效率低,通常只能到达毫秒级。)
接口
#include <sys/eventfd.h>
int eventfd(unsigned int initval, int flags);
这个函数会创建一个事件对象(eventfd object),返回一个文件描述符,用来实现进程或线程间的等待/通知(wait/notify)机制。内核为这个对象维护了一个无符号的64位整形计数器 counter,用第一个参数(initval)初始化这个计数器,创建时一般可将其设为0,后面有例子测试这个参数产生的效果。
flags 可以使用三个宏:
EFD_CLOEXEC:给这个新的文件描述符设置 FD_CLOEXEC 标志,即 close-on-exec,这样在调用 exec 后会自动关闭文件描述符。因为通常执行另一个程序后,会用全新的程序替换子进程的正文,数据,堆和栈等,原来的文件描述符变量也不存在了,这样就没法关闭没用的文件描述符了。
EFD_NONBLOCK:设置文件描述符为非阻塞的,设置了这个标志后,如果没有数据可读,就返回一个 EAGAIN 错误,不会一直阻塞。
EFD_SEMAPHORE:这个标志位会影响read操作,具体可以看read方法中的解释。
提供的方法
从上面可以看出来,eventfd 支持三种操作:read、write、close。
read 返回值的情况如下:
- 读取 8 字节值,如果当前 counter > 0,那么返回 counter 值,并重置 counter 为 0。(设置了EFD_SEMAPHORE标志位,则返回1,且计数器中的值也减去1。没有设置EFD_SEMAPHORE标志位,则返回计数器中的值,且计数器置0。)
- 如果调用 read 时 counter 为 0,那么 1)阻塞直到 counter 大于 0;2)非阻塞,直接返回 -1,并设置 errno 为 EAGAIN。如果 buffer 的长度小于 8 字节,那么 read 会失败,并设置 errno 为 EINVAL。
可以看出来 eventfd 只允许一次 read,对应两种状态:0和非0。下面看下 write。
write :
- 写入一个 64 bit(8字节)的整数 value 到 eventfd。
- 返回值:counter 最大能存储的值是 0xffff ffff ffff fffe,write 尝试将 value 加到 counter 上,如果结果超过了 max,那么 write 一直阻塞直到 read 操作发生,或者返回 -1 并设置 errno 为 EAGAIN。
可多次 write,一次 read。close 就是关掉 fd。
以上大概就是我了解的 eventfd,它相比于 pipe来说,少用了一个文件描述符,而且不必管理缓冲区,单纯的事件通知的话,方便很多(它的名字就叫做 eventfd),它可以和事件通知机制很好的融合。
看个线程间唤醒的例子:
/*
* @filename: eventfd_pthread.c
* @author: Tanswer
* @date: 2018年01月08日 22:46:38
* @description:
*/
#include <stdio.h>
#include <stdlib.h>
#include <sys/eventfd.h>
#include <pthread.h>
#include <unistd.h>
int efd;
void *threadFunc()
{
uint64_t buffer;
int rc;
while(1){
rc = read(efd, &buffer, sizeof(buffer)); //一次就会将多次对他写入的所有值的和读出来,类似管道
if(rc == 8){ //起到一个唤醒线程的作用
printf("notify success\n");
}
printf("rc = %llu, buffer = %lu\n",(unsigned long long)rc, buffer);
}//end while
}
int main()
{
pthread_t tid;
int rc;
uint64_t buf = 1;
efd = eventfd(0,0); // blocking
if(efd == -1){
perror("eventfd");
}
//create thread
if(pthread_create(&tid, NULL, threadFunc, NULL) < 0){
perror("pthread_create");
}
while(1){
rc = write(efd, &buf, sizeof(buf));
if(rc != 8){
perror("write");
}
sleep(2);
}//end while
close(efd);
return 0;
}
我们程序中具体使用时,是让eventloop线程阻塞在epoll上,当有事件来了,就写eventfd唤醒他(用channel)
这里有一篇文章讲得比较细:
http://blog.chinaunix.net/uid-25929161-id-3781524.html
struct eventfd_ctx {
struct kref kref; /* 这个就不多说了,file计数用的,用于get/put */
wait_queue_head_t wqh; /* 这个用来存放用户态的进程wait项,有了它通知机制才成为可能 */
/*
* Every time that a write(2) is performed on an eventfd, the
* value of the __u64 being written is added to "count" and a
* wakeup is performed on "wqh". A read(2) will return the "count"
* value to userspace, and will reset "count" to zero. The kernel
* side eventfd_signal() also, adds to the "count" counter and
* issue a wakeup.
*/
__u64 count; /* 这个就是一个技术器,应用程序可以自己看着办,read就是取出然后清空,write就是把value加上 */
unsigned int flags; /* 所有的file都有的吧,用来存放阻塞/非阻塞标识或是O_CLOEXEC之类的东西 */
};
// This function is supposed to be called by the kernel in paths that do not
// allow sleeping. In this function we allow the counter to reach the ULLONG_MAX
// value, and we signal this as overflow condition by returining a POLLERR to poll(2).
int eventfd_signal(struct eventfd_ctx *ctx, int n) //本质上是做一个唤醒
{
unsigned long flags;
if (n < 0)
return -EINVAL;
spin_lock_irqsave(&ctx->wqh.lock, flags);
if (ULLONG_MAX - ctx->count < n)
n = (int) (ULLONG_MAX - ctx->count);
ctx->count += n;
if (waitqueue_active(&ctx->wqh))
wake_up_locked_poll(&ctx->wqh, POLLIN);
spin_unlock_irqrestore(&ctx->wqh.lock, flags);
return n;
}
当内核态想通知用户态时,直接使用eventfd_signal,此时用户态线程需要先把自己放在eventfd_ctx->wqh上,有两种方案,一个是调用read,一个是调用poll。 如果是read,之后会将eventfd_ctx->count清零,下次还能阻塞住。但是如果使用poll,之后count并未清零,导致再次poll时,即使内核态没有eventfd_signal,poll也会即时返回。
具体实现原理详解见腾讯云社区https://cloud.tencent.com/developer/article/1160842
_ _thread
是GCC内置的线程局部存储设施,存取效率可以和全局变量相比。_ _thread变量每一个线程有一份独立实体,各个线程的值互不干扰。可以用来修饰那些带有全局性且值可能变,但是又不值得用全局变量保护的变量。通过 _ _thread
修饰的变量,在线程中地址都不一样,__thread
变量每一个线程有一份独立实体,各个线程的值互不干扰。
__thread EventLoop* t_loopInThisThread = 0;
if (t_loopInThisThread) {
//LOG << "Another EventLoop " << t_loopInThisThread << " exists in this thread " << threadId_;
}
else {
t_loopInThisThread = this;
}
用来维护每个线程中存在的唯一EventLoop。
右值引用与move
原文:https://blog.youkuaiyun.com/p942005405/article/details/84644069
在C++11中,标准库在<utility>中提供了一个有用的函数std::move,std::move并不能移动任何东西,它唯一的功能是将一个左值强制转化为右值引用,继而可以通过右值引用使用该值,以用于移动语义。从实现上讲,std::move基本等同于一个类型转换:static_cast<T&&>(lvalue);
1. C++ 标准库使用比如vector::push_back 等这类函数时,会对参数的对象进行复制,连数据也会复制.这就会造成对象内存的额外创建, 本来原意是想把参数push_back进去就行了,通过std::move,可以避免不必要的拷贝操作。
2. std::move是将对象的状态或者所有权从一个对象转移到另一个对象,只是转移,没有内存的搬迁或者内存拷贝所以可以提高利用效率,改善性能.。
3. 对指针类型的标准库对象并不需要这么做.
用法:
原lvalue值被moved from之后值被转移,所以为空字符串.
//摘自https://zh.cppreference.com/w/cpp/utility/move
#include <iostream>
#include <utility>
#include <vector>
#include <string>
int main()
{
std::string str = "Hello";
std::vector<std::string> v;
//调用常规的拷贝构造函数,新建字符数组,拷贝数据
v.push_back(str);
std::cout << "After copy, str is \"" << str << "\"\n";
//调用移动构造函数,掏空str,掏空后,最好不要使用str
v.push_back(std::move(str));
std::cout << "After move, str is \"" << str << "\"\n";
std::cout << "The contents of the vector are \"" << v[0]
<< "\", \"" << v[1] << "\"\n";
}
输出:
After copy, str is "Hello"
After move, str is ""
The contents of the vector are "Hello", "Hello"
std::move 的函数原型定义
template <typename T>
typename remove_reference<T>::type&& move(T&& t)
{
return static_cast<typename remove_reference<T>::type&&>(t);
还有一些详细说明见原文。
emplace_back
https://blog.youkuaiyun.com/p942005405/article/details/84764104
c++开发中我们会经常用到插入操作对stl的各种容器进行操作,比如vector,map,set等。在引入右值引用,转移构造函数,转移复制运算符之前,通常使用push_back()向容器中加入一个右值元素(临时对象)时,首先会调用构造函数构造这个临时对象,然后需要调用拷贝构造函数将这个临时对象放入容器中。原来的临时变量释放。这样造成的问题就是临时变量申请资源的浪费。
引入了右值引用,转移构造函数后,push_back()右值时就会调用构造函数和转移构造函数,如果可以在插入的时候直接构造,就只需要构造一次即可。这就是c++11 新加的emplace_back。
emplace_back函数原型:
template <class... Args>
void emplace_back (Args&&... args);
在容器尾部添加一个元素,这个元素原地构造,不需要触发拷贝构造和转移构造。而且调用形式更加简洁,直接根据参数初始化临时对象的成员。
一个很有用的例子:
#include <vector>
#include <string>
#include <iostream>
struct President
{
std::string name;
std::string country;
int year;
President(std::string p_name, std::string p_country, int p_year)
: name(std::move(p_name)), country(std::move(p_country)), year(p_year)
{
std::cout << "I am being constructed.\n";
}
President(const President& other)
: name(std::move(other.name)), country(std::move(other.country)), year(other.year)
{
std::cout << "I am being copy constructed.\n";
}
President(President&& other)
: name(std::move(other.name)), country(std::move(other.country)), year(other.year)
{
std::cout << "I am being moved.\n";
}
President& operator=(const President& other);
};
int main()
{
std::vector<President> elections;
std::cout << "emplace_back:\n";
elections.emplace_back("Nelson Mandela", "South Africa", 1994); //没有类的创建
std::vector<President> reElections;
std::cout << "\npush_back:\n";
reElections.push_back(President("Franklin Delano Roosevelt", "the USA", 1936));
std::cout << "\nContents:\n";
for (President const& president: elections) {
std::cout << president.name << " was elected president of "
<< president.country << " in " << president.year << ".\n";
}
for (President const& president: reElections) {
std::cout << president.name << " was re-elected president of "
<< president.country << " in " << president.year << ".\n";
}
}
输出:
emplace_back:
I am being constructed.
push_back:
I am being constructed.
I am being moved.
Contents:
Nelson Mandela was elected president of South Africa in 1994.
可以看到,emplace_back只调用了一次构造函数,而push_back先调用构造函数创建一个临时变量,再调用move复制到容器中,最后还要析构临时变量。
在这里不得不说说移动构造函数和拷贝构造函数的区别:
https://blog.youkuaiyun.com/sinat_25394043/article/details/78728504
移动构造是C++11标准中提供的一种新的构造方法。
在现实中有很多这样的例子,我们将钱从一个账号转移到另一个账号,将手机SIM卡转移到另一台手机,将文件从一个位置剪切到另一个位置……
移动构造可以减少不必要的复制,带来性能上的提升。
有些复制构造是必要的,我们确实需要另外一个副本;而有些复制构造是不必要的,我们可能只是希望这个对象换个地方,移动一下而已。
在C++11之前,如果要将源对象的状态转移到目标对象只能通过复制。
而现在在某些情况下,我们没有必要复制对象——只需要移动它们。
C++11引入移动语义: 源对象资源的控制权全部交给目标对象
对比一下复制构造和移动构造:
复制构造是这样的:
在对象被复制后临时对象和复制构造的对象各自占有不同的同样大小的堆内存,就是一个副本。
移动构造是这样的:
就是让这个临时对象它原本控制的内存的空间转移给构造出来的对象,这样就相当于把它移动过去了。
复制构造和移动构造的差别:
这种情况下,我们觉得这个临时对象完成了复制构造后,就不需要它了,我们就没有必要去首先产生一个副本,然后析构这个临时对象,这样费两遍事,又占用内存空间,所幸将临时对象它的原本的资源直接转给构造的对象即可了。
当临时对象在被复制后,就不再被利用了。我们完全可以把临时对象的资源直接移动,这样就避免了多余的复制构造。
什么时候该触发移动构造呢?
如果临时对象即将消亡,并且它里面的资源是需要被再利用的,这个时候我们就可以触发移动构造。
移动构造是需要通过移动构造函数来完成的。
移动构造函数定义形式:
class_name(class_name && )
例:函数返回含有指针成员的对象
有两种版本:
版本一:使用深层复制构造函数
~ 返回时构造临时对象,动态分配临时对象返回到主调函数,然后删除临时对象。
版本二:使用移动构造函数
~ 将要返回的局部对象转移到主调函数,省去了构造和删除临时对象的过过程。
实例程序:
https://blog.youkuaiyun.com/carbon06/article/details/81222759
#include <vector>
#include <cstring>
#include <iostream>
#include<string>
class A
{
public:
A(const int size) : size(size)
{
if (size)
{
data = new char[size];
}
std::cout << "I'm constructor.\n";
}
A(const A& other)
{
size = other.size;
data = new char[size];
memcpy(data, other.data, size * sizeof(char));
std::cout << "I'm copy constructor.\n";
}
A(A&& other)
{
size = other.size;
data = other.data;
other.size = 0;
other.data = nullptr;
std::cout << "I'm move constructor.\n";
}
private:
int size;
char* data = nullptr;
};
int main()
{
std::vector<A> vec;
vec.reserve(1024);
A tmp(5);
std::cout << "push_back a left value.\n";
vec.push_back(tmp);
std::cout << "push_back a right value with std::move.\n";
vec.push_back(std::move(tmp));
std::cout << "emplace_back a left value.\n";
vec.emplace_back(tmp);
std::cout << "emplace_back a right value with std::move.\n";
vec.emplace_back(std::move(tmp));
std::cout << "emplace_back in place.\n";
vec.emplace_back(5);
std::cout << "=========================================\n";
std::cout << "test with buildin string move and emplace_back\n";
std::cout << "=========================================\n";
std::vector<std::string> str_vec;
str_vec.reserve(1024);
std::string str = "I'd like to be inserted to a container";
std::cout << "before emplace_back to vec, str is:\n";
std::cout << str << std::endl;
std::cout << "c_str address is " << (void*)str.c_str() << std::endl;
str_vec.emplace_back(std::move(str));
std::cout << "after emplace_back to vec, str is:\n";
std::cout << str << std::endl;
std::cout << "c_str address is " << (void*)str.c_str() << std::endl;
std::cout << "c_str address of the string in container is "
<< (void*)str_vec.front().c_str() << std::endl;
system("pause");
return 0;
}
输出结果:
从执行结果中,我们可以得出以下结论
1. push_back 可以接收左值也可以接受右值,接收左值时使用拷贝构造,接收右值时使用移动构造
2. emplace_back 接收右值时调用类的移动构造
3. emplace_back 接收左值时,实际上的执行效果是先对传入的参数进行拷贝构造,然后使用拷贝构造后的副本,也就是说,emplace_back在接收一个左值的时候其效果和push_back一致!所以在使用emplace_back 时需要确保传入的参数是一个右值引用,如果不是,请使用std::move()进行转换
4. emplace_back 接收多个参数时,可以调用匹配的构造函数实现在容器内的原地构造
5. 使用string 类验证了移动构造函数式对类成员所有权的传递,从上图中看到string 在插入前c_str的地址和使用emplace_back 移动到容器后的c_str的地址一致。并且移动后字符串c_str 的地址指向其他位置。
SIGPIPE
SIGPIPE的默认行为是终止进程,在命令行程序中这是合理的,但是在网络编程中,这意味着如果对方断开连接而本地继续写入的话,会造成服务进程意外退出(先收到RST再收到SIGPIPE)。
假如服务器进程繁忙,没有及时处理对方断开连接的事件,就有可能出现在连接断开之后继续发送数据的情况。
解决的办法很简单,在程序开始时就忽略SIFPIPE即可
具体是在创建服务器对象
Server myHTTPServer(&mainLoop, threadNum, port);
时,在其构造函数中调用 handle_for_sigpipe();函数:
void handle_for_sigpipe() {
struct sigaction sa;
memset(&sa, '\0', sizeof(sa));
sa.sa_handler = SIG_IGN;
sa.sa_flags = 0;
if (sigaction(SIGPIPE, &sa, NULL))
return;
}
限制服务器的最大并发连接数
我们这里讨论的“并发连接数”是指一个服务端程序能同时支持的客户端连接数,连接由客户端主动发起,服务端被动接受连接(accept).
为什么要限制并发连接数?
一方面,我们不希望服务程序超载,另一方面更因为file descriptor是稀缺资源,如果出现fd耗尽的情况,比较棘手。就跟“malloc()失败抛出bad_alloc差不多棘手”。
假如有这样一种场景:
我们创建了一个listenfd挂在epoll上(水平触发形式),当调用epoll_wait获得新连接事件时,用accept去处理listenfd上的事件,但是此时本进程的文件描述符满了,accpet返回EMFILE,无法为新连接创建socket文件描述符。 但是,既然没有socket描述符来表示这个连接,我们也没法close他,程序就继续运行。当再一次epoll_wait时会立即返回(因为新连接还等待处理,listenfd还是可读的),这样的话程序就会陷入busy loop,CPU占用率接近100%。这既影响了同一event loop上的连接,也影响了同一机器上的其他服务。
该如何解决呢?书上写了几种做法:
1.调高进程的文件描述符数目(治标不治本)
2.死等(鸵鸟算法)
3.退出程序(小题大作)
4.关闭listenfd(但是什么时候重新打开呢?)
5.改用edge trigger(如果漏掉了一次accpet,程序就再也不会受到新连接 why?)
6.准备一个空的文件描述符。遇到这种情况,先关闭这个空闲文件,获得一个fd名额,再accept拿到新的socket fd(可读写的),随后理科close他,这样就优雅地断开了客户端的连接,最后再重新打开一个空闲文件,把“坑”占住,以备再次出现这种情况时使用。
另外还有一种比较简单的做法,fd是hard limit,我们可以自己设置一个稍低一点的soft limit,如果超过 soft limit就主动关闭新连接。比方说当前进程的max fd 是1024,我们可以在连接数达到1000时就进入“拒绝新连接”的状态,这样就可以留给我们足够的腾挪空间。
tcp半关闭
在TCP服务端和客户端建立连接之后服务端和客户端会分别有两个独立的输入流和输出流,而且相互对应。服务端的输出流对应于客户端的输入流,服务端的输入流对应于客户端的输出流。这是在建立连接之后的状态。
当我们调用close()函数时,系统会同时把双方的输入流和输出流全部关闭,但是有时候我们仍需要在一方断开连接之后只进行接受数据或者传输数据其中一项操作。这时就需要我们只断开输入或者输出,保留另一个流的正常运转,也就引入了TCP的半关闭状态。
基本操作:
之前我们传输完数据之后便直接调用了close()函数,我们可以使用系统提供的shutdown()函数方便的完成TCP的半关闭。
shutdown(int socket , int type):半关闭套接字中的输入或者输出流
- socket(套接字描述符):需要断开的套接字描述符
- type(断开类型):套接字的断开方式
SHUT_RD——断开输入流,并清空输入缓冲中的数据
SHUT_WR——断开输出流,并将输出缓冲中的数据输出
SHUT_RDWR——同时断开输入输出流,分两次调用shutdown()函数
成功时返回0,失败时返回-1
例子:
/**
在数据输出完成之后,对输出流进行流半关闭
这种状态下服务读不能向客户端写入数据,但是可以接受来自客户端的数据
**/
shutdown(client_sock,SHUT_WR);
//接受来自客户端的消息
while(0 == read(client_sock,buff,BUFF_SIZE))
{
continue;
}
//最后完全关闭
close(client_sock);
这里提一句RST
导致“Connection reset”的原因是服务器端因为某种原因关闭了Connection,而客户端依然在读写数据,此时服务器会返回复位标志“RST”,然后此时客户端就会提示“Connection reset”。
可能有同学对复位标志“RST”还不太了解,这里简单解释一下:
TCP建立连接时需要三次握手,在释放连接需要四次挥手;例如三次握手的过程如下:
第一次握手:客户端发送syn包(syn=j)到服务器,并进入SYN_SENT状态,等待服务器确认;
第二次握手:服务器收到syn包,并会确认客户的SYN(ack=j+1),同时自己也发送一个SYN包(syn=k),即SYN+ACK包,此时服务器进入SYN_RECV状态;
第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=k+1),此包发送完毕,客户端和服务器进入ESTABLISHED(TCP连接成功)状态,完成三次握手。
可以看到握手时会在客户端和服务器之间传递一些TCP头信息,比如ACK标志、SYN标志以及挥手时的FIN标志等。
除了以上这些常见的标志头信息,还有另外一些标志头信息,比如推标志PSH、复位标志RST等。其中复位标志RST的作用就是“复位相应的TCP连接”。
莫要神话RST,其实他就是tcp报文段首部里面的一个标志位而已,共有如下几个:
URG,ACK,PSH,RST,SYN,FIN
还有一种比较常见的错误“Connection reset by peer”,该错误和“Connection reset”是有区别的:
服务器返回了“RST”时,如果此时客户端正在从Socket套接字的输出流中读数据则会提示Connection reset”;
服务器返回了“RST”时,如果此时客户端正在往Socket套接字的输入流中写数据则会提示“Connection reset by peer”。
有几种情况会收到RST:
1.
客户端、服务器端TCP连接一切正常,TCP连接由于没有数据传输而出于空闲(Idle)状态。突然服务器掉电,当服务器重新启动完毕,与客户端的TCP连接状态由于掉电而完全消失。之后,客户端发给服务器任何消息,都会触发服务器发RST作为回应。
服务器之所以发RST,是因为连接不存在,通过Reset状态位,间接告诉客户端异常情况的存在。
a. Reset顺利到达客户端
客户端意识到异常发生了,会立马释放该TCP连接所占用的内存资源(状态、数据)、以及端口号。客户端TCP会通知数据的主人(应用程序),由于TCP连接被对方Reset,数据发送失败。客户端无需超时等待,立即使用原有端口号,重新发起一个TCP连接,双方状态再一次同步,进入正常通信状态。
b.
Reset没有到达客户端
客户端的状态依然为“established”状态,反正双方的状态已经不同步了,如果客户端有数据、或keepalive要发送,会继续触发服务器发送Reset。这种情况是由于外界因素影响,使得双方状态不同步,一方为“established”,另一方为“closed”, 重置(Reset)连接状态是最好的方法!有读者会问,如果客户端一直没有消息发给服务器端,那双方状态的不同步是否会保持到天长地久?
会的!
但是考虑到当前的网络状况,这种可能性是比较小的,因为目前的网络NAT无处不在,为了克服NAT表项没有流量刷新而删除NAT表项,进而影响客户端、服务器端通信,如今的TCP实现会在几十秒发送一次Keepalive,这样即使没有用户流量,Keepalive也会刷新NAT表项,从而避免NAT设备删表操作。所以双方通信不同步状态,在当今的TCP实现上会很快监测到、并予以纠正。
2.
当客户端发起一个TCP连接请求,途径公司防火墙时,防火墙查询自己的安全策略,这是一个不被允许的连接请求,于是防火墙以服务器IP的名义,返还给用户一个Reset状态位,用户以为是服务器发的,其实服务器压根不知道,是防火墙作为中间人发的。
优雅关闭连接
TCP连接断开的时候调用close socket函数,已经讨论过有优雅的断开和强制断开,那么如何设置断开连接的方式呢?是通过设置socket描述符一个linger结构体属性。
linger结构体数据结构如下:
struct linger
{
int l_onoff;// 表示是否立即关闭连接,0表示不立即关闭,即优雅方式;非零表示立即关闭连接,即强制关闭
int l_linger;//表示优雅方式关闭连接的等待时间
};
有三种组合方式:
第一种
l_onoff = 0;
l_linger忽略
这种方式下,就是在closesocket的时候立刻返回,底层会将未发送完的数据发送完成后再释放资源,也就
是优雅的退出。但是这里有一个副作用就是socket的底层资源会被保留直到TCP连接关闭,这个时间用户应用程序是无法控制的。
第二种
l_onoff非零
l_linger = 0;
这种方式下,在调用closesocket的时候同样会立刻返回,但不会发送未发送完成的数据,而是通过一个RST包强制的关闭socket描述符,也就是强制的退出。
第三种
l_onoff非零
l_linger > 0
这种方式下,在调用closesocket的时候不会立刻返回,内核会延迟一段时间,这个时间就由l_linger的值来决定。如果超时时间到达之前,发送完未发送的数据(包括FIN包)并得到另一端的确认,closesocket会返回正确,socket描述符优雅性退出。否则,closesocket会直接返回错误值,未发送数据丢失,socket描述符被强制性退出。需要注意的是,如果socket描述符被设置为非堵塞型,则closesocket会直接返回。此种情况下,应用程序检查close的返回值是非常重要的,如果在数据发送完并被确认前时间到,close将返回EWOULDBLOCK(EAGAIN)错误且套接口发送缓冲区中的任何数据都丢失。close的成功返回仅告诉我们发送的数据(和FIN)已由对方TCP确认,它并不能告诉我们对方应用进程是否已读了数据。
程序中:
void setSocketNoLinger(int fd)
{
struct linger linger_;
linger_.l_onoff = 1;
linger_.l_linger = 30;
setsockopt(fd, SOL_SOCKET, SO_LINGER,(const char *) &linger_, sizeof(linger_));
}
注意SO_LINGER和SO_DONTLINGER选项只影响closesocket的行为,而与shutdown函数无关,shutdown总是会立即返回的。
关闭tcp nagle算法
Nagle算法是以他的发明人John Nagle的名字命名的,它用于自动连接许多的小缓冲器消息;这一过程(称为nagling)通过减少必须发送包的个数来增加网络软件系统的效率。Nagle算法于1984年定义为福特航空和通信公司IP/TCP拥塞控制方法,这是福特经营的最早的专用TCP/IP 网络减少拥塞控制,从那以后这一方法得到了广泛应用。Nagle的文档里定义了处理他所谓的小包问题的方法,这种问题指的是应用程序一次产生一字节数据, 这样会导致网络由于太多的包而过载(一个常见的情况是发送端的"愚蠢窗口综合症")。从键盘输入的一个字符,占用一个字节,可能在传输上造成41字节的 包,其中包括1字节的有用信息和40字节的标题数据。这种情况转变成了4000%的消耗,这样的情况对于轻负载的网络来说还是可以接受的,但是重负载的福 特网络就受不了了,它没有必要在经过节点和网关的时候重发,导致包丢失和妨碍传输速度。吞吐量可能会妨碍甚至在一定程度上会导致连接失败。Nagle的算 法通常会在TCP程 序里添加两行代码,在未确认数据发送的时候让发送器把数据送到缓存里。任何数据随后继续直到得到明显的数据确认或者直到攒到了一定数量的数据了再发包。尽管Nagle的算法解决的问题只是局限于福特网络,然而同样的问题也可能出现在ARPANet。这种方法在包括因特网在内的整个网络里得到了推广,成为了 默认的执行方式,尽管在高互动环境下有些时候是不必要的,例如在客户/服务器情形下。在这种情况下,nagling可以通过使用TCP_NODELAY 插座选项关闭。
1. nagle算法主要目的是减少网络流量,当你发送的数据包太小时,TCP并不立即发送该数据包,而是缓存起来直到数据包
到达一定大小后才发送。
2. 当应用程序每次发送的数据很小,特别是只发送1个字节,加上TCP和IP头的封装,TCP头占20个字节,IP头也占20个字 节,这时候发一个包是41个字节,效率太低。而nagle算法允许计算机缓冲数据,当数据缓存到一定长度后,如果之前发送 的数据得到了ACK确认且接收方有足够空间容纳数据 (当然也要考虑MSS),就发送这些数据,否则继续等待。
3. TCP socket提供了关闭nagle算法的接口,可以通过TCP_NODELAY选项决定是否开启该算法。
程序中:
void setSocketNodelay(int fd) {
int enable = 1;
setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, (void*)&enable, sizeof(enable));
}
开启端口复用:
// 消除bind时"Address already in use"错误
int optval = 1;
if(setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval)) == -1)
return -1;
INADDR_ANY:
INADDR_ANY就是指定地址为0.0.0.0的地址,这个地址事实上表示不确定地址,或“所有地址”、“任意地址”。 一般来说,在各个系统中均定义成为0值。
一般情况下,如果你要建立网络服务器应用程序,则你要通知服务器操作系统:请在某地址 xxx.xxx.xxx.xxx上的某端口 yyyy上进行侦听,并且把侦听到的数据包发送给我。这个过程,你是通过bind()系统调用完成的。——也就是说,你的程序要绑定服务器的某地址,或者说:把服务器的某地址上的某端口占为已用。服务器操作系统可以给你这个指定的地址,也可以不给你。
如果你的服务器有多个网卡(每个网卡上有不同的IP地址),而你的服务(不管是在udp端口上侦听,还是在tcp端口上侦听),出于某种原因:可能是你的服务器操作系统可能随时增减IP地址,也有可能是为了省去确定服务器上有什么网络端口(网卡)的麻烦 —— 可以要在调用bind()的时候,告诉操作系统:“我需要在 yyyy 端口上侦听,所有发送到服务器的这个端口,不管是哪个网卡/哪个IP地址接收到的数据,都是我处理的。”这时候,服务器程序则在0.0.0.0这个地址上进行侦听。
// 设置服务器IP和Port,和监听描述符绑定
struct sockaddr_in server_addr;
bzero((char*)&server_addr, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
server_addr.sin_port = htons((unsigned short)port);
if (bind(listen_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1)
return -1;
将线程锁封装为一个类
class MutexLockGuard : noncopyable
{
public:
explicit MutexLockGuard(MutexLock &_mutex) :
mutex(_mutex)
{
mutex.lock();
}
~MutexLockGuard()
{
mutex.unlock();
}
private:
MutexLock &mutex;
};
之前一直不理解muduo为啥要这样做。。。还有函数中为啥还能有第二个{}对
今天在写这个成员函数时突然想通了,不知道理解得对不对:
EventLoop* EventLoopThread::startLoop() {
assert(!thread_.started());
thread_.start();
{
MutexLockGuard lock(mutex_); //将锁搞成一个类,在函数段内创建临时锁对象并加锁,当退出函数段时自动调用对象析构函数进行解锁
// 一直等到threadFun在Thread里真正跑起来
while (loop_ == NULL)
cond_.wait(); //等在条件变量上
}
return loop_;
}
函数中的第二对{}规定了一个小的生命周期,在此{}开始处创建临时锁对象lock并加锁,当退出此{}时,会析构这个临时锁对象,并在他的析构函数中去调用mutex.unlock() 进行解锁。666,我之前还一直奇怪只看到加锁没看到解锁呀。。。
explicit -> C++提供了关键字explicit,可以阻止不应该允许的经过转换构造函数进行的隐式转换的发生。声明为explicit的构造函数不能在隐式转换中使用。
例:
class Test1
{
public:
Test1(int n)
{
num=n;
}//普通构造函数
private:
int num;
};
class Test2
{
public:
explicit Test2(int n)
{
num=n;
}//explicit(显式)构造函数
private:
int num;
};
int main()
{
Test1 t1=12;//隐式调用其构造函数,成功
Test2 t2=12;//编译错误,不能隐式调用其构造函数
Test2 t2(12);//显式调用成功
return 0;
}
Test1的构造函数带一个int型的参数,代码23行会隐式转换成调用Test1的这个构造函数。而Test2的构造函数被声明为explicit(显式),这表示不能通过隐式转换来调用这个构造函数,因此代码24行会出现编译错误。
普通构造函数能够被隐式调用。而explicit构造函数只能被显式调用。
C++11 中std::function和std::bind的用法
https://blog.youkuaiyun.com/liukang325/article/details/53668046
关于std::function 的用法:
其实就可以理解成函数指针
- 保存自由函数
void printA(int a)
{
cout<<a<<endl;
}
std::function<void(int a)> func;
func = printA;
func(2);
- 保存lambda表达式
std::function<void()> func_1 = [](){cout<<"hello world"<<endl;};
func_1();
- 保存成员函数
struct Foo {
Foo(int num) : num_(num) {}
void print_add(int i) const { cout << num_+i << '\n'; }
int num_;
};
// 保存成员函数
std::function<void(const Foo&, int)> f_add_display = &Foo::print_add;
Foo foo(2);
f_add_display(foo, 1);
在实际使用中都用 auto 关键字来代替std::function… 这一长串了。
关于std::bind 的用法:
看一系列的文字,不如看一段代码理解的快
#include <iostream>
#include<functional>
using namespace std;
class A
{
public:
void fun_3(int k,int m)
{
cout<<k<<" "<<m<<endl;
}
};
void fun(int x,int y,int z)
{
cout<<x<<" "<<y<<" "<<z<<endl;
}
void fun_2(int &a,int &b)
{
a++;
b++;
cout<<a<<" "<<b<<endl;
}
int main(int argc, const char * argv[])
{
auto f1 = std::bind(fun,1,2,3); //表示绑定函数 fun 的第一,二,三个参数值为: 1 2 3
f1(); //print:1 2 3
auto f2 = std::bind(fun, placeholders::_1,placeholders::_2,3);
//表示绑定函数 fun 的第三个参数为 3,而fun 的第一,二个参数分别有调用 f2 的第一,二个参数指定
f2(1,2);//print:1 2 3
auto f3 = std::bind(fun,placeholders::_2,placeholders::_1,3);
//表示绑定函数 fun 的第三个参数为 3,而fun 的第一,二个参数分别有调用 f3 的第二,一个参数指定
//注意: f2 和 f3 的区别。
f3(1,2);//print:2 1 3
int n = 2;
int m = 3;
auto f4 = std::bind(fun_2, n,placeholders::_1);
f4(m); //print:3 4
cout<<m<<endl;//print:4 说明:bind对于不事先绑定的参数,通过std::placeholders传递的参数是通过引用传递的
cout<<n<<endl;//print:2 说明:bind对于预先绑定的函数参数是通过值传递的
A a;
auto f5 = std::bind(&A::fun_3, a,placeholders::_1,placeholders::_2);
f5(10,20);//print:10 20
std::function<void(int,int)> fc = std::bind(&A::fun_3, a,std::placeholders::_1,std::placeholders::_2);
fc(10,20);//print:10 20
return 0;
}
具体到我们写的程序中呢,就是用这两个工具给新建的循环线程绑定运行函数。程序中不使用虚函数这种多态机制,具体分析见陈硕大佬的《linux多线程服务端编程》p.447
bind的设计思想;
高内聚,低耦合,使被调用的函数和调用者完全隔离开来.调用者可以根据需要任意设计接口,和传参,而被调用函数通过bind可以不经修改接口就可以兼容各种需求的变化.
区别于静态绑定,动态绑定,这属于程序员自动绑定.
线程的栈
在很多现代操作系统中,一个进程的(虚)地址空间大小为4G,分为系统(内核?)空间和用户空间两部分,系统空间为所有进程共享,而用户空间是独立的,一般linux进程的用户空间为3G。
进程简说:
进程就是程序的一次执行。
进程是为了在CPU上实现多道编程而发明的一个概念。
事实上我们说线程是进程里面的一个执行上下文,或者执行序列,显然一个进程可以同时拥有多个执行序列,更加详细的描述是,舞台上有多个演员同时出场,而这些演员和舞台就构成了一出戏,类比进程和线程,每个演员是一个线程,舞台是地址空间,这个同一个地址空间里面的所有线程就构成了进程。
比如当我们打开一个word程序,其实已经同时开启了多个线程,这些线程一个负责显示,一个接受输入,一个定时进行存盘,这些线程一起运转让我们感到我们的输入和屏幕显示同时发生,而不用键入一些字符等好长时间才能显示到屏幕上。
我们知道,线程共享着一个进程内的堆、全局变量、静态变量(bss和data段)、.text段、文件描述符表等,但是线程拥有自己的pcb(线程id等)、栈、寄存器、程序计数器等。
我们可以看到,每个线程拥有自己的栈,栈中存放着他调用的函数开辟的栈帧,每个栈帧中存放着一些局部变量和临时值。其中临时值存放了我调用下一个函数开辟栈帧之前的ebp和esp指向的位置,在函数调用完毕后,才知道要从哪里继续执行。
这里有一个线程号的概念--
线程号是内核用来分配时间轮片时要用的
线程id是进程内部区分线程时用到的。
linux下查看一个进程开辟了多少线程以及他们的线程号:
ps -Lf 3500(pid)
其中LWP列就是线程号
这里特别说一下pthread_join的返回值:
int pthread_join(pthread_t thread, void **rval_ptr);
用主线程去等待子线程pthread_exit时用到这个函数,thread是目标线程标识符,rval_ptr指向目标线程返回时的退出信息(因为pthread_exit退出的返回值是void*型的,所以要用void**型变量去接受它),该函数会一直阻塞,直到被回收的线程结束为止。
返回的退出信息可以说普通变量也可以是我们自定义的变量:
1.返回普通变量:
2.返回自定义变量如结构体:
其中,传进线程函数的arg就是我们在创建线程时传进的结构体,见下面程序:
pthread_detach:
当在一个线程中通过调用pthread_join()来回收资源时,调用者就会被阻塞,如果需要回收的线程数目过多时,效率就大大下降。比如在一个Web 服务器中, 主线程为每一个请求创建一个线程去响应动作,我们并不希望主线程也为了回收资源而被阻塞,因为可能在阻塞的同时有新的请求,我们可以再使用下面的方法,让线程办完事情后自动回收资源。
1 ). 在子线程中调用pthread_detach( pthread_self() )。
2 ).在主线程中调用pthread_detach( tid )。
可以将线程状态设为分离。运行结束后会自动释放所有资源。
注意,在子线程detach之后,别的线程是不能去join他的,会一直返回error=22这个错误。
#include <stdio.h>
#include <pthread.h>
void* run(void * arg)
{
pthread_detach( pthread_self());
printf("I will detach .. \n");
return NULL;
}
int main()
{
pthread_t tid1;
pthread_create(&tid1, NULL, run, NULL);
//或者:
//pthread_detach(tid1);
sleep(1); // 因为主线程不会挂起等待,为了保证子线程先执行完分离,让主线程先等待1s
int ret = 0;
ret = pthread_join(tid1, NULL);
if( ret == 0)
{
printf(" join sucess. \n");
}
else
{
printf(" join failed. \n");
}
return 0;
}
今天碰到了一个问题:
虚表是属于类的还是属于对象的
https://blog.youkuaiyun.com/lihao21/article/details/50688337
先说结论:虚表是属于类的,而不是属于某个具体的对象,一个类只需要一个虚表即可。同一个类的所有对象都使用同一个虚表。
为了指定对象的虚表,对象内部包含一个虚表的指针,来指向自己所使用的虚表。为了让每个包含虚表的类的对象都拥有一个虚表指针,编译器在类中添加了一个指针,*__vptr,用来指向虚表。这样,当类的对象在创建时便拥有了这个指针,且这个指针的值会自动被设置为指向类的虚表。
注意:虚表是一个指针数组,其元素是虚函数的指针,每个元素对应一个虚函数的函数指针。需要指出的是,普通的函数即非虚函数,其调用并不需要经过虚表,所以虚表的元素并不包括普通函数的函数指针。
虚表内的条目,即虚函数指针的赋值发生在编译器的编译阶段,也就是说在代码的编译阶段,虚表就可以构造出来了。
当一个类(A)继承另一个类(B)时,类A会继承类B的函数的调用权。所以如果一个基类包含了虚函数,那么其继承类也可调用这些虚函数,换句话说,一个类继承了包含虚函数的基类,那么这个类也拥有自己的虚表。
C++的编译器应该是保证虚函数表的指针存在于对象实例中最前面的位置(这是为了保证取到虚函数表的有最高的性能——如果有多层继承或是多重继承的情况下)。 这意味着我们通过对象实例的地址得到这张虚函数表,然后就可以遍历其中函数指针,并调用相应的函数。因此可以有下面实验代码:
https://blog.youkuaiyun.com/qq_33657884/article/details/81745009
class classA {
virtual void function() {
}
};
int main()
{
classA *a = new classA();
printf("%x\n", *(int*)(void*)a);
for (int i = 0; i < 10000;i++) {
classA *b = new classA();
if (*(int*)(void*)a == *(int*)(void*)b) {
printf("一样的虚函数表地址\n");
}
else {
printf("不一样的虚函数表地址\n");
break;
}
delete b;
}
return 0;
}
打印结果是满屏的一样的虚函数表地址,所以结论是虚函数表是属于一类的
多线程编程之锁和条件变量
之前学习这部分内容的时候只是快速过一遍,等真正写程序要用的时候发现全忘了。。。
果然没有自己动手写过的东西不是真正的掌握呀。。。
互斥锁mutex:
互斥锁是通过锁的机制来实现线程间的同步问题。互斥锁的基本流程为:
1.初始化一个互斥锁:pthread_mutex_init()函数
2.加锁:pthread_mutex_lock()函数或者pthread_mutex_trylock()函数
3.对共享资源的操作
4.解锁:pthread_mutex_unlock()函数
5.注销互斥锁:pthread_mutex_destory()函数
其中,在加锁过程中,pthread_mutex_lock()函数和pthread_mutex_trylock()函数的过程略有不同:
当使用pthread_mutex_lock()函数进行加锁时,若此时已经被锁,则尝试加锁的线程会被阻塞,直到互斥锁被其他线程释放,当pthread_mutex_lock()函数有返回值时,说明加锁成功;
而使用pthread_mutex_trylock()函数进行加锁时,若此时已经被锁,则会返回EBUSY的错误码(轮询方式进行加锁)。
同时,解锁的过程中,也需要满足两个条件:
解锁前,互斥锁必须处于锁定状态;
必须由加锁的线程进行解锁。
当互斥锁使用完成后,必须进行清除。
api如下:成功返回0,失败返回错误号
#include <pthread.h>
#include <time.h>
// 初始化一个互斥锁。
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
// 对互斥锁上锁,若互斥锁已经上锁,则调用者一直阻塞,
// 直到互斥锁解锁后再上锁。
int pthread_mutex_lock(pthread_mutex_t *mutex);
// 调用该函数时,若互斥锁未加锁,则上锁,返回 0;
// 若互斥锁已加锁,则函数直接返回失败,即 EBUSY。
int pthread_mutex_trylock(pthread_mutex_t *mutex);
// 当线程试图获取一个已加锁的互斥量时,pthread_mutex_timedlock 互斥量
// 原语允许绑定线程阻塞时间。即非阻塞加锁互斥量。
int pthread_mutex_timedlock(pthread_mutex_t *restrict mutex,
const struct timespec *restrict abs_timeout);
// 对指定的互斥锁解锁。
int pthread_mutex_unlock(pthread_mutex_t *mutex);
// 销毁指定的一个互斥锁。互斥锁在使用完毕后,
// 必须要对互斥锁进行销毁,以释放资源。
int pthread_mutex_destroy(pthread_mutex_t *mutex);
使用例子:
可以看到,先在全局区定义一个mutex,保证这个锁是能被主线程和子线程共享的。然后在主线程中先初始化锁(mutex=1)再创建子线程,接着就让主线程和子线程都循环,去争夺cpu和锁。
注意,在线程中访问完临界区后,应该立即释放锁(此处临界区为stdout,就是往屏幕输出),这样才能尽可能地将锁的粒度减小。
死锁现象:
1.线程对同一互斥量加锁两次:第一次加锁成功,第二次欲加锁时,发现有人锁上了于是阻塞等待那个人解锁,但是加锁的人又是他自己,所以造成了死锁(我等我自己)。
2.线程1拥有A锁,请求获得B锁,而线程2拥有B锁,请求获得A锁(使用try_lock,与不能获得所有的锁时,主动放弃已占有的锁去成全别人)
条件变量
原文:https://blog.youkuaiyun.com/daaikuaichuan/article/details/82950711
与互斥锁不同,条件变量是用来等待而不是用来上锁的。条件变量用来自动阻塞一个线程,直到某特殊情况发生为止。通常条件变量和互斥锁同时使用。
条件变量使我们可以睡眠等待某种条件出现。条件变量是利用线程间共享的全局变量进行同步的一种机制,主要包括两个动作:
1. 一个线程等待"条件变量的条件成立"而挂起;
2. 另一个线程使 “条件成立”(给出条件成立信号)。
【原理】:
条件的检测是在互
斥锁的保护下进行的。线程在改变条件状态之前必须首先锁住互斥量。如果一个条件为假,一个线程自动阻塞,并释放等待状态改变的互斥锁。如果另一个线程改变了条件,它发信号给关联的条件变量,唤醒一个或多个等待它的线程,重新获得互斥锁,重新评价条件。如果两进程共享可读写的内存,条件变量 可以被用来实现这两进程间的线程同步。
【条件变量的操作流程如下】:
1. 初始化:init()或者pthread_cond_tcond=PTHREAD_COND_INITIALIER;属性置为NULL;
2. 等待条件成立:pthread_wait,pthread_timewait.wait()释放锁,并阻塞等待条件变量为真 timewait()设置等待时间,仍未signal,返回ETIMEOUT(加锁保证只有一个线程wait);
3. 激活条件变量:pthread_cond_signal,pthread_cond_broadcast(激活所有等待线程)
4. 清除条件变量:destroy;无线程等待,否则返回EBUSY清除条件变量:destroy;无线程等待,否则返回EBUSY
api:
#include <pthread.h>
// 初始化条件变量
int pthread_cond_init(pthread_cond_t *cond,pthread_condattr_t *cond_attr);
// 阻塞等待
int pthread_cond_wait(pthread_cond_t *cond,pthread_mutex_t *mutex);
// 超时等待
int pthread_cond_timewait(pthread_cond_t *cond,pthread_mutex *mutex,const timespec *abstime);
// 解除所有线程的阻塞
int pthread_cond_destroy(pthread_cond_t *cond);
// 唤醒至少一个等待该条件的线程
int pthread_cond_signal(pthread_cond_t *cond);
// 唤醒等待该条件的所有线程
int pthread_cond_broadcast(pthread_cond_t *cond);
条件变量实现生产者消费者模型:
在调用pthread_cond_wait之前,应该提前做好
1.创建锁,初始化锁
2.创建条件变量,初始化条件变量
3.线程加上锁,然后调用pthread_cond_wait。
例子:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
//链表作为共享数据,需要被互斥锁保护
struct msg {
struct msg* next;
int num;
};
struct msg* head;
struct msg* mp;
//静态初始化条件变量和互斥锁
pthread_cond_t has_product = PTHREAD_COND_INITIALIZER;
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
void* consumer(void* p) {
for (;;) {
pthread_mutex_lock(&lock);
while (head == NULL) { //头指针为空,说明没有节点
pthread_cond_wait(&has_product, &lock);
}
mp = head;
head = mp->next; //模拟消费掉一个产品
pthread_mutex_unlock(&lock);
printf("consume ---- %d\n", mp->num);
free(mp);
_sleep(rand() % 5);
}
}
void* producer(void* p) {
for (;;) {
mp = (struct msg*) malloc(sizeof(struct msg));
mp->num = rand() % 1000 + 1;//模拟生产一个产品
printf("produce ---%d\n", mp->num);
pthread_mutex_lock(&lock);
mp->next = head;
head = mp;
pthread_mutex_unlock(&lock);
pthread_cond_signal(&has_product);//将等待在该条件变量上的一个线程唤醒
_sleep(rand() % 5);
}
}
int main(int argc, char* argv[]) {
pthread_t pid, cid;
srand(time(NULL));
pthread_create(&pid, NULL, producer, NULL);
pthread_create(&cid, NULL, consumer, NULL);
pthread_join(pid, NULL);
pthread_join(cid, NULL);
return 0;
}
其中,pthread_cond_wait做了三件事:1.解锁 2.让线程等在条件变量上(等pthread_cond_signal)3.等到时将锁再加上
这个while比较关键,因为往往有许多消费者线程等在条件变量上,而一次来signal时只有一个线程能抢到锁(即wait的第3步),那么这个抢到的线程跳出了while循环去做完业务之后,条件变量又会变为原样(如head==NULL)。那么其他被唤醒的线程在抢到锁后(第3步),就还要判断一次条件变量,否则可能误跳出while,去做业务了(此处造成的后果可能是访问不存在的链表节点)。
在创建线程时,给指定的线程函数传入参数:
1.传入单个值:
2.传入多个值,应该要创建一个结构体,再把结构体传进去
noncopyable类
在程序中定义了一个共有父类叫做noncopyable:
class noncopyable
{
protected:
noncopyable() = default;
~noncopyable() = default;
private:
noncopyable(const noncopyable&);
const noncopyable& operator=(const noncopyable&);
};
注意其拷贝构造函数和重载等号运算符是私有的,这保证了他的子类无法等号赋值和拷贝构造:
#include "noncopyable.h"
class non :noncopyable
{
public:
non() {
}
~non() {
}
};
int main(){
non a;
non b(a);
non c;
c = a;
return 0;
}
epoll_create1(int flag)
epoll的接口非常简单,一共就3/4个函数:
int epoll_create(int size);
int epoll_create1(int flags);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
对于epoll_create1 的flag参数: 可以设置为0 或EPOLL_CLOEXEC,为0时函数表现与epoll_create()一致, EPOLL_CLOEXEC标志与open 时的O_CLOEXEC 标志类似,即进程被替换时会关闭打开的文件描述符,即在另一个进程获得cpu时,自动将本描述符关闭,以免发生误操作(需要注意的是,epoll_create与epoll_create1当创建好epoll句柄后,它就是会占用一个fd值,在linux下如果查看/proc/<pid>/fd/,是能够看到这个fd的,所以在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽)。
最初的epoll_create实现中,size参数告诉内核,调用者期望添加到epoll实例中的文件描述符数量。内核使用该信息作为初始分配内部数据结构空间大小的“提示”(如果调用者的使用超过了size,则内核会在必要的情况下分配更多的空间)。目前,这种“提示”已经不再需要了,内核动态的改变数据结构的大小,但是为了保证向后兼容性(新的epoll应用运行于旧的内核上),size参数还是要大于0。
这里“多路”指的是多个网络连接,“复用”指的是复用同一个线程。采用多路 I/O 复用技术可以让单个线程高效的处理多个连接请求
pthread_once
https://blog.youkuaiyun.com/zhangxiao93/article/details/51910043
#include <pthread.h>
int pthread_once(pthread_once_t *once_control, void (*init_routine) (void));
使用前要先在外部静态初始化一个控制变量 once_control 如
pthread_once_t once = PTHREAD_ONCE_INIT
在多线程环境中,有些事仅需要执行一次。通常当初始化应用程序时,可以比较容易地将其放在main函数中。但当你写一个库时,就不能在main里面初始化了,你可以用静态初始化,但使用一次初始化(pthread_once)会比较容易些。
在多线程编程环境下,尽管pthread_once()调用会出现在多个线程中,init_routine()函数仅执行一次,究竟在哪个线程中执行是不定的,是由内核调度来决定。
每次线程欲调用init_routine()时,都会先检查once_control变量是否为初始值,若不为初始值(已被执行过)就简单返回。
一个测试:
#include<iostream>
#include<pthread.h>
#include <unistd.h>
using namespace std;
pthread_once_t once = PTHREAD_ONCE_INIT;
void once_run(void)
{
cout<<"once_run in thread "<<(unsigned int )pthread_self()<<endl;
}
void * child1(void * arg)
{
pthread_t tid =pthread_self();
cout<<"thread "<<(unsigned int )tid<<" enter"<<endl;
pthread_once(&once,once_run);
cout<<"thread "<<tid<<" return"<<endl;
}
void * child2(void * arg)
{
pthread_t tid =pthread_self();
cout<<"thread "<<(unsigned int )tid<<" enter"<<endl;
pthread_once(&once,once_run);
cout<<"thread "<<tid<<" return"<<endl;
}
int main(void)
{
pthread_t tid1,tid2;
cout<<"hello"<<endl;
pthread_create(&tid1,NULL,child1,NULL);
pthread_create(&tid2,NULL,child2,NULL);
pthread_join(tid1,NULL);
pthread_join(tid2,NULL);
cout<<"main thread exit"<<endl;
return 0;
}
程序执行后,线程中指定函数部分在两个子线程中出现,不过只执行一次。
hello
thread 3086535584 enter
once_run in thread 3086535584
thread 3086535584 return
thread 3076045728 enter
thread 3076045728 return
main thread exit
线程安全的单例模式
使用pthread_once来保护对象申请,来保证“单例”,及时在多线程的情况下,这是muduo库作者的思路。
代码如下:
//muduo/base/Singleton.h
template<typename T>
class Singleton : boost::noncopyable
{
public:
static T& instance()
{
pthread_once(&ponce_, &Singleton::init);
assert(value_ != NULL);
return *value_;
}
private:
Singleton();
~Singleton();
static void init()
{
value_ = new T();
if (!detail::has_no_destroy<T>::value)
{
::atexit(destroy);
}
}
static void destroy()
{
typedef char T_must_be_complete_type[sizeof(T) == 0 ? -1 : 1];
T_must_be_complete_type dummy; (void) dummy;
delete value_;
value_ = NULL;
}
private:
static pthread_once_t ponce_;
static T* value_;
};
enable_shared_from_this
https://blog.youkuaiyun.com/caoshangpa/article/details/79392878
enable_shared_from_this是一个模板类,定义于头文件<memory>,其原型为:
template< class T > class enable_shared_from_this;
std::enable_shared_from_this 能让一个对象(假设其名为 t ,且已被一个 std::shared_ptr 对象 pt 管理)安全地生成其他额外的 std::shared_ptr 实例(假设名为 pt1, pt2, ... ) ,它们与 pt 共享对象 t 的所有权。
若一个类 T 继承 std::enable_shared_from_this<T> ,则会为该类 T 提供成员函数: shared_from_this 。 当 T 类型对象 t 被一个为名为 pt 的 std::shared_ptr<T> 类对象管理时,调用 T::shared_from_this 成员函数,将会返回一个新的 std::shared_ptr<T> 对象,它与 pt 共享 t 的所有权。
使用场景:https://www.cnblogs.com/mkdym/p/4947296.html
熟悉异步编程的同学可能会对boost::shared_from_this有所了解。我们在传入回调的时候,通常会想要其带上当前类对象的上下文,或者回调本身就是类成员函数,那这个工作自然非this指针莫属了,像这样:
void sock_sender::post_request_no_lock()
{
Request &req = requests_.front();
boost::asio::async_write(*sock_ptr_,
boost::asio::buffer(req.buf_ptr->get_content()),
boost::bind(&sock_sender::self_handler, this, _1, _2));
}
然而回调执行的时候并不一定对象还存在。为了确保对象的生命周期大于回调,我们可以使类继承自boost::enable_shared_from_this,然后回调的时候使用boost::bind传入shared
_from_this()返回的智能指针。由于boost::bind保存的是参数的副本,bind构造的函数对象会一直持有一个当前类对象的智能指针而使得其引用计数不为0,这就确保了对象的生存周期大于回调中构造的函数对象的生命周期,像这样:
class sock_sender
: public boost::enable_shared_from_this<sock_sender>
{
//...
};
void sock_sender::post_request_no_lock()
{
Request &req = requests_.front();
boost::asio::async_write(*sock_ptr_,
boost::asio::buffer(req.buf_ptr->get_content()),
boost::bind(&sock_sender::self_handler, shared_from_this(), _1, _2));
}
我们知道,当类继承自boost::enable_shared_from_this后,类便不能再创建栈上对象了,必须new。然而,代码却并没有阻止我们创建栈上对象,使用这个类的人若不清楚这点,很可能就会搞错,导致运行时程序崩溃。(这里待研究)
这里https://www.cnblogs.com/codingmengmeng/p/9123874.html讲了具体原理
实践中使用场景:
class HttpData : public std::enable_shared_from_this<HttpData>
{
public:
...
}
loop_->runInLoop(bind(&HttpData::handleClose, shared_from_this()));
手工处理HTTP请求头:
自己手工处理一次来知道原来是这么回事
URIState HttpData::parseURI() {
string& str = inBuffer_; //引用
string cop = str;
// 读到完整的请求行再开始解析请求
size_t pos = str.find('\r', nowReadPos_); //'\r' 回车,回到当前行的行首,而不会换到下一行,如果接着输出的话,本行以前的内容会被逐一覆盖;
//windows下'\n' 换行,换到当前位置的下一行,而不会回到行首;
if (pos < 0) {
return PARSE_URI_AGAIN;
}
// 去掉请求行所占的空间,节省空间
string request_line = str.substr(0, pos); //request_line保存请求内容
if (str.size() > pos + 1)
str = str.substr(pos + 1); //截去请求部分
else
str.clear();
//Method //判断请求的方法
int posGet = request_line.find("GET");
int posPost = request_line.find("POST");
int posHead = request_line.find("HEAD");
if (posGet >= 0) {
pos = posGet;
method_ = METHOD_GET;
}
else if (posPost >= 0)
{
pos = posPost;
method_ = METHOD_POST;
}
else if (posHead >= 0)
{
pos = posHead;
method_ = METHOD_HEAD;
}
else
{
return PARSE_URI_ERROR;
}
// filename
pos = request_line.find("/", pos);
if (pos < 0) {
fileName_ = "index_html";
HTTPVersion_ = HTTP_11;
return PARSE_URI_SUCCESS;
}
else {
size_t _pos = request_line.find(' ', pos);
if(_pos < 0)
return PARSE_URI_ERROR;
else {
if (_pos - pos > 1) { //在pos之后找到了空格
fileName_ = request_line.substr(pos + 1, _pos - pos - 1);//手工截取fileName_
size_t __pos = fileName_.find('?');
if (__pos >= 0) //有问号的话,后面部分舍去
{
fileName_ = fileName_.substr(0, __pos);
}
}
else
fileName_ = "index.html";
}
pos = _pos; //能走到这说明_pos>0,是有效值
}
//cout << "fileName_: " << fileName_ << endl;
// HTTP 版本号
pos = request_line.find("/", pos);
if (pos < 0)
return PARSE_URI_ERROR;
else {
if (request_line.size() - pos <= 3) //不够版本号长度
return PARSE_URI_ERROR;
else {
string ver = request_line.substr(pos + 1, 3);
if(ver=="1.0")
HTTPVersion_ = HTTP_10;
else if (ver == "1.1")
HTTPVersion_ = HTTP_11;
else
return PARSE_URI_ERROR;
}
}
return PARSE_URI_SUCCESS;
}
日志模块:
看linya大哥的github:https://github.com/linyacool/WebServer/blob/master/WebServer/base/Log%E7%9A%84%E8%AE%BE%E8%AE%A1.txt
Log的设计仿照了muduo库的设计,但我写的没那么复杂
https://github.com/chenshuo/muduo
与Log相关的类包括FileUtil、LogFile、AsyncLogging、LogStream、Logging。
其中前4个类每一个类都含有一个append函数,Log的设计也是主要围绕这个append函数展开的。
FileUtil是最底层的文件类,封装了Log文件的打开、写入并在类析构的时候关闭文件,底层使用了标准IO,该append函数直接向文件写。
LogFile进一步封装了FileUtil,并设置了一个循环次数,没过这么多次就flush一次。
AsyncLogging是核心,它负责启动一个log线程,专门用来将log写入LogFile,应用了“双缓冲技术”,其实有4个以上的缓冲区,但思想是一样的。
AsyncLogging负责(定时到或被填满时)将缓冲区中的数据写入LogFile中。
LogStream主要用来格式化输出,重载了<<运算符,同时也有自己的一块缓冲区,这里缓冲区的存在是为了缓存一行,把多个<<的结果连成一块。
Logging是对外接口,Logging类内涵一个LogStream对象,主要是为了每次打log的时候在log之前和之后加上固定的格式化的信息,比如打log的行、
文件名等信息。
stat
表头文件: #include <sys/stat.h>
#include <unistd.h>
定义函数: int stat(const char *file_name, struct stat *buf);
函数说明: 通过文件名filename获取文件信息,并保存在buf所指的结构体stat中
返回值: 执行成功则返回0,失败返回-1,错误代码存于errno
错误代码:
ENOENT 参数file_name指定的文件不存在
ENOTDIR 路径中的目录存在但却非真正的目录
ELOOP 欲打开的文件有过多符号连接问题,上限为16符号连接
EFAULT 参数buf为无效指针,指向无法存在的内存空间
EACCESS 存取文件时被拒绝
ENOMEM 核心内存不足
ENAMETOOLONG 参数file_name的路径名称太长
例子:
#include <sys/stat.h>
#include <unistd.h>
#include <stdio.h>
int main() {
struct stat buf;
stat("/etc/hosts", &buf);
printf("/etc/hosts file size = %d/n", buf.st_size);
}
其中
struct stat {
dev_t st_dev; //文件的设备编号
ino_t st_ino; //节点
mode_t st_mode; //文件的类型和存取的权限
nlink_t st_nlink; //连到该文件的硬连接数目,刚建立的文件值为1
uid_t st_uid; //用户ID
gid_t st_gid; //组ID
dev_t st_rdev; //(设备类型)若此文件为设备文件,则为其设备编号
off_t st_size; //文件字节数(文件大小)
unsigned long st_blksize; //块大小(文件系统的I/O 缓冲区大小)
unsigned long st_blocks; //块数
time_t st_atime; //最后一次访问时间
time_t st_mtime; //最后一次修改时间
time_t st_ctime; //最后一次改变时间(指属性)
};
inode相关知识参见:讲的非常好
https://www.cnblogs.com/xiexj/p/7214502.html
FCB(file control block)文件控制块,是文件系统的一部分。在磁盘上通常会创建一个文件系统,文件系统中包括文件夹信息。以及文件的FCB信息。FCB一半包括文件的读写模式。全部者,时间戳,数据块指针等信息。unix的FCB称为inode。其结构例如以下图所看到的
文件打开的步骤例如以下图所看到的(从右往左看)
首先,操作系统依据文件名称a,在系统文件打开表中查找
第一种情况:
假设文件a已经打开。则在进程文件打开表中为文件a分配一个表项,然后将该表项的指针指向系统文件打开表中和文件a相应的一项;
然后在PCB中为文件分配一个文件描写叙述符fd,作为进程文件打开表项的指针,文件打开完毕。
另外一种情况:
假设文件a没有打开。查看含有文件a信息的文件夹项是否在内存中。假设不在,将文件夹表(目录表)装入到内存中,作为cache。
依据文件夹表(目录表)中文件a相应项找到FCB(inode)在磁盘中的位置。
将文件a的FCB装入到内存中的Active inode中。
然后在系统文件打开表中为文件a添加新的一个表项,将表项的指针指向Active Inode中文件a的FCB;
然后在进程的文件打开表中分配新的一项,将该表项的指针指向系统文件打开表中文件a相应的表项。
然后在PCB中,为文件a分配一个文件描写叙述符fd,作为进程文件打开表项的指针,文件打开完毕。
sprintf和read、write
sprintf
指的是字符串格式化命令,主要功能是把格式化的数据写入某个字符串中。sprintf 是个变参函数。使用sprintf 对于写入buffer的字符数是没有限制的,这就存在了buffer溢出的可能性。解决这个问题,可以考虑使用 snprintf函数,该函数可对写入字符数做出限制。
函数功能:把格式化的数据写入某个字符串
函数原型:int sprintf( char *buffer, const char *format [, argument] … );
返回值:字符串长度(strlen)
例子:
char s[50];
char* who = "I";
char* whom = "优快云";
sprintf(s, "%s love %s.", who, whom); //产生:"I love 优快云. " 这字符串写到s中
sprintf(s,"%s%d%c","test",1,'2'); //会覆盖,应该s+12
read
ssize_t read [1] (int fd, void *buf, size_t count);
read()会把参数fd 所指的文件传送count个字节到buf指针所指的内存中。若参数count为0,则read为实际读取到的字节数,如果返回0,表示已到达文件尾或是无可读取的数据,此外文件读写位置会随读取到的字节移动。
如果顺利read()会返回实际读到的字节数,最好能将返回值与参数count 作比较,若返回的字节数比要求读取的字节数少,则有可能读到了文件尾、从管道(pipe)或终端机读取,或者是read()被信号中断了读取动作。当有错误发生时则返回-1,错误代码存入errno中,而文件读写位置则无法预期。
错误代码
EINTR 此调用被信号所中断。
EAGAIN 或者EWOULDBLOCK,当使用非阻塞I/O 时(O_NONBLOCK == 非阻塞),若读缓冲区为空则返回此值。
非阻塞socket直接忽略;如果是阻塞的socket,一般是读写操作超时了,还未返回。这个超时是指socket的SO_RCVTIMEO与SO_SNDTIMEO两个属性。所以在使用阻塞socket时,不要将超时时间设置的过小。不然返回了-1,你也不知道是socket连接是真的断开了,还是正常的网络抖动。一般情况下,阻塞的socket返回了-1,都需要关闭重新连接。
EBADF 参数fd 非有效的文件描述词,或该文件已关闭。
ECONNRESET:
1、在客户端服务器程序中,客户端异常退出,并没有回收关闭相关的资源,服务器端再写会先收到ECONNRESET错误,然后收到EPIPE错误。
或者是linger选项中设置了l_onoff=>0且linger=0,则调用socket时本端会强制关闭socket,丢弃缓冲区内数据并且向对方发生RST分节,则对方将read返回-1且错误码为ECONNRESET。
2、连接被远程主机关闭。有以下几种原因:远程主机停止服务,重新启动;当在执行某些操作时遇到失败,因为设置了“keep alive”选项,连接被关闭,一般与ENETRESET一起出现。
3、远程端执行了一个“hard”或者“abortive”的关闭。应用程序应该关闭socket,因为它不再可用。当执行在一个UDP socket上时,这个错误表明前一个send操作返回一个ICMP“port unreachable”信息。
4、如果client关闭连接,server端的select并不出错(不返回-1,使用select对唯一一个socket进行non- blocking检测),但是写该socket就会出错,用的是send.错误号:ECONNRESET.读(recv)socket并没有返回错误。
5、该错误被描述为“connection reset by peer”,即“对方复位连接”,这种情况一般发生在服务进程较客户进程提前终止。当服务进程终止时会向客户 TCP 发送 FIN 分节,客户 TCP 回应 ACK,服务 TCP 将转入 FIN_WAIT2 状态。此时如果客户进程没有处理该 FIN (如阻塞在其它调用上而没有关闭 Socket 时),则客户 TCP 将处于 CLOSE_WAIT 状态。当客户进程再次向 FIN_WAIT2 状态的服务 TCP 发送数据时,则服务 TCP 将立刻响应 RST。一般来说,这种情况还可以会引发另外的应用程序异常,客户进程在发送完数据后,往往会等待从网络IO接收数据,很典型的如 read 或 readline 调用,此时由于执行时序的原因,如果该调用发生在 RST 分节收到前执行的话,那么结果是客户进程会得到一个非预期的 EOF 错误。此时一般会输出“server terminated prematurely”-“服务器过早终止”错误。
ENETRESET
网络重置时丢失连接。
由于设置了"keep-alive"选项,探测到一个错误,连接被中断。在一个已经失败的连接上试图使用setsockopt操作,也会返回这个错误。
write
ssize_t write (int fd,const void * buf,size_t count);
write()会把指针buf所指的内存写入count个字节到参数fd所指的文件内。当然,文件读写位置也会随之移动。
返回值
如果顺利write()会返回实际写入的字节数。当有错误发生时则返回-1,错误代码存入errno中。
错误代码
EINTR 此调用被信号所中断。
EAGAIN 当使用非阻塞I/O 时(O_NONBLOCK),若写缓冲区已满则返回此值。
EBADF 参数fd非有效的文件描述词,或该文件已关闭。
EPIPE:
1、Socket 关闭,但是socket号并没有置-1。继续在此socket上进行send和recv,就会返回这种错误。这个错误会引发SIGPIPE信号,系统会将产生此EPIPE错误的进程杀死。所以,一般在网络程序中,首先屏蔽此消息,以免发生不及时设置socket进程被杀死的情况。
2、write(..) on a socket that has been closed at the other end will cause a SIGPIPE.
3、错误被描述为“broken pipe”,即“管道破裂”,这种情况一般发生在客户进程不理会(或未及时处理)Socket 错误,继续向服务 TCP 写入更多数据时,内核将向客户进程发送 SIGPIPE 信号,该信号默认会使进程终止(此时该前台进程未进行 core dump)。结合上边的 ECONNRESET 错误可知,向一个 FIN_WAIT2 状态的服务 TCP(已 ACK 响应 FIN 分节)写入数据不成问题,但是写一个已接收了 RST 的 Socket 则是一个错误。
(对端已经关闭,再向他写则收到对端的rst分节,再写则收到本端内核的sigpipe信号)
mmap(一种内存映射文件的方法)
(进程间通信的共享内存)
mmap将一个文件或者其它对象映射进内存。文件被映射到多个页上,如果文件的大小不是所有页的大小之和,最后一个页不被使用的空间将会清零。mmap在用户空间映射调用系统中作用很大。
//头文件
#include<sys/mman.h>
//函数原型
void* mmap(void* start,size_t length,int prot,int flags,int fd,off_t offset);
int munmap(void* start,size_t length);
条件:mmap() [1] 必须以PAGE_SIZE为单位进行映射,而内存也只能以页为单位进行映射,若要映射非PAGE_SIZE整数倍的地址范围,要先进行内存对齐,强行以PAGE_SIZE的倍数大小进行映射。
参数说明
start:映射区的开始地址,设置为0时表示由系统决定映射区的起始地址。
length:映射区的长度。//长度单位是 以字节为单位,不足一内存页按一内存页处理
prot:期望的内存保护标志,不能与文件的打开模式冲突。是以下的某个值,可以通过or运算合理地组合在一起
PROT_EXEC //页内容可以被执行
PROT_READ //页内容可以被读取
PROT_WRITE //页可以被写入
PROT_NONE //页不可访问
flags:指定映射对象的类型,映射选项和映射页是否可以共享。它的值可以是一个或者多个以下位的组合体
MAP_FIXED //使用指定的映射起始地址,如果由start和len参数指定的内存区重叠于现存的映射空间,重叠部分将会被丢弃。如果指定的起始地址不可用,操作将会失败。并且起始地址必须落在页的边界上。
MAP_SHARED //与其它所有映射这个对象的进程共享映射空间。对共享区的写入,相当于输出到文件。直到msync()或者munmap()被调用,文件实际上不会被更新。
MAP_PRIVATE //建立一个写入时拷贝的私有映射。内存区域的写入不会影响到原文件。这个标志和以上标志是互斥的,只能使用其中一个。
MAP_DENYWRITE //这个标志被忽略。
MAP_EXECUTABLE //同上
MAP_NORESERVE //不要为这个映射保留交换空间。当交换空间被保留,对映射区修改的可能会得到保证。当交换空间不被保留,同时内存不足,对映射区的修改会引起段违例信号。
MAP_LOCKED //锁定映射区的页面,从而防止页面被交换出内存。
MAP_GROWSDOWN //用于堆栈,告诉内核VM系统,映射区可以向下扩展。
MAP_ANONYMOUS //匿名映射,映射区不与任何文件关联。
MAP_ANON //MAP_ANONYMOUS的别称,不再被使用。
MAP_FILE //兼容标志,被忽略。
MAP_32BIT //将映射区放在进程地址空间的低2GB,MAP_FIXED指定时会被忽略。当前这个标志只在x86-64平台上得到支持。
MAP_POPULATE //为文件映射通过预读的方式准备好页表。随后对映射区的访问不会被页违例阻塞。
MAP_NONBLOCK //仅和MAP_POPULATE一起使用时才有意义。不执行预读,只为已存在于内存中的页面建立页表入口。
fd:有效的文件描述词。一般是由open()函数返回,其值也可以设置为-1,此时需要指定flags参数中的MAP_ANON,表明进行的是匿名映射。
off_toffset:被映射对象内容的起点。
返回值:
成功执行时,mmap()返回被映射区的指针,munmap()返回0。失败时,mmap()返回MAP_FAILED[其值为(void *)-1],munmap返回-1。errno被设为以下的某个值
EACCES:访问出错
EAGAIN:文件已被锁定,或者太多的内存已被锁定
EBADF:fd不是有效的文件描述符
EINVAL:一个或者多个参数无效
ENFILE:已达到系统对打开文件的限制
ENODEV:指定文件所在的文件系统不支持内存映射
ENOMEM:内存不足,或者进程已超出最大内存映射数量
EPERM:权能不足,操作不允许
ETXTBSY:已写的方式打开文件,同时指定MAP_DENYWRITE标志
SIGSEGV:试着向只读区写入
SIGBUS:试着访问不属于进程的内存区
- 系统调用mmap()用于共享内存的两种方式:
(1)使用普通文件提供的内存映射:适用于任何进程之间;此时,需要打开或创建一个文件,然后再调用mmap();典型调用代码如下:
fd=open(name, flag, mode);
if(fd<0)
...
ptr=mmap(NULL, len , PROT_READ|PROT_WRITE, MAP_SHARED , fd , 0); 通过mmap()实现共享内存的通信方式有许多特点和要注意的地方,我们将在范例中进行具体说明。
(2)使用特殊文件提供匿名内存映射:适用于具有亲缘关系的进程之间;由于父子进程特殊的亲缘关系,在父进程中先调用mmap(),然后调用fork()。那么在调用fork()之后,子进程继承父进程匿名映射后的地址空间,同样也继承mmap()返回的地址,这样,父子进程就可以通过映射区域进行通信了。注意,这里不是一般的继承关系。一般来说,子进程单独维护从父进程继承下来的一些变量。而mmap()返回的地址,却由父子进程共同维护。
对于具有亲缘关系的进程实现共享内存最好的方式应该是采用匿名内存映射的方式。此时,不必指定具体的文件,只要设置相应的标志即可
- 系统调用munmap()
int munmap( void * addr, size_t len )
该调用在进程地址空间中解除一个映射关系,addr是调用mmap()时返回的地址,len是映射区的大小。当映射关系解除后,对原来映射地址的访问将导致段错误发生。
- 系统调用msync()
int msync ( void * addr , size_t len, int flags)
一般说来,进程在映射空间的对共享内容的改变并不直接写回到磁盘文件中,往往在调用munmap()后才执行该操作。可以通过调用msync()实现磁盘上文件内容与共享内存区的内容一致。
实际使用:用来打开服务器上的文件,并读取出内容,再发送给请求的客户端:
int src_fd = open(file_name.c_str(), O_RDONLY, 0); //打开请求资源文件
char *src_addr = static_cast<char*>(mmap(NULL, sbuf.st_size, PROT_READ, MAP_PRIVATE, src_fd, 0));
close(src_fd);
// 发送文件并校验完整性
send_len = writen(fd, src_addr, sbuf.st_size);
if(send_len != sbuf.st_size)
{
perror("Send file failed");
return ANALYSIS_ERROR;
}
munmap(src_addr, sbuf.st_size); //解除一个映射关系
return ANALYSIS_SUCCESS;
后台开发工程师技术能力体系图: