mysql之锁机制

mysql默认采用的是InnoDB,默认可以支持row-level-lock,在InnoDB中,有两种锁,读共享锁和写锁,读共享锁可以多个读操作一起进行,写锁只允许一个客户端独占,由于支持的是行锁,所以在查找的时候需要有明确的主键信息,才可以
确定是那一行,然后进行加锁,没有的话,mysql会直接锁表。
读共享锁:

1
select * from tableA where id=*** LOCK IN SHARE MODE

写锁:

1
2
3
//必须指明主主键信息,才能明确到行,如果信息不能确定到具体的行的话,只能锁表,如id <> 3,
//也会直接锁表. FOR UPDATE 仅适用于InnoDB,且必须在事务区块(BEGIN/COMMIT)中才能生效。
select * from tableA where id=*** FOR UPDATE

事务:

mysql默认采用的是自动提交模式,可以采用下面的命令来修改:

1
2
set autocommit=0  //取消自动提交
set autocommit=1 //自动提交

自动提交表示你每输入一条命令,myswl自动的执行 commit,所以每一条命令对应一个事务,如果需要多条命令作为事务的话,那么需要:

1
2
3
4
5
6
BEGIN
命令1
命令2
...
...
commit

一个事务由BEGIN开始,commit结束。事务只保证所有提交的命令的执行原子性,并不能用来作为同步,默认情况下,事务是可以一起执行的,例如事务A和B都需要读取行记录r,两个事务可以并发执行。有串行模式的事务,只支持同时只有一个
事务运行,这可以用来实现同步,但是效率很低,因为一个表同时只能有一个操作。

Linux命令之top

显示进程占用cpu以及内存的情况

1
2
3
4
5
6
7
8
-d: 表示每一次刷新的时间间隔
-c: 显示整个命令行(包括参数)而不只是显示命令名
-p: 只显示具体的进程,不显示所有的进程
在top的显示过程中,还可以输入命令进行交互:

M:以内存的占有率进行排序
P:CPU占有率进行排序
T:运行时间进行排序
d:改变刷新时间

示例:

1
2
3
4
5
top   //每隔5秒显式所有进程的资源占用情况
top -d 2 //每隔2秒显式所有进程的资源占用情况
top -c //每隔5秒显式进程的资源占用情况,并显示进程的命令行参数(默认只有进程名)
top -p 12345 -p 6789//每隔5秒显示pid是12345和pid是6789的两个进程的资源占用情况
top -d 2 -c -p 123456 //每隔2秒显示pid是12345的进程的资源使用情况,并显式该进程启动的命令行参数

Linux命令之netstat

从整体上看,netstat的输出结果可以分为两个部分:

一个是Active Internet connections,称为有源TCP连接,其中”Recv-Q”和”Send-Q”指%0A的是接收队列和发送队列。这些数字一般都应该是0。如果不是则表示软件包正在队列中堆积。这种情况只能在非常少的情况见到。

另一个是Active UNIX domain sockets,称为有源Unix域套接口(和网络套接字一样,但是只能用于本机通信,性能可以提高一倍)。
Proto显示连接使用的协议,RefCnt表示连接到本套接口上的进程号,Types显示套接口的类型,State显示套接口当前的状态,Path表示连接到套接口的其它进程使用的路径名。

常见参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
-a (all)显示所有选项,默认不显示LISTEN相关
-t (tcp)仅显示tcp相关选项
-u (udp)仅显示udp相关选项
-x Unix域socket,仅显示Unxi域相关的信息
-n 拒绝显示别名,能显示数字的全部转化成数字。
-l 仅列出有在 Listen (监听) 的服務状态

-p 显示建立相关链接的程序名
-r 显示路由信息,路由表
-e 显示扩展信息,例如uid等
-s 按各个协议进行统计
-c 每隔一个固定时间,执行该netstat命令。定时执行netcat
-i 显示网络接口列表
-ie 两个加起显示类时于ifconfig一样的详细信息

提示:LISTEN和LISTENING的状态只有用-a或者-l才能看到

Linux命令之netcat

netcat是网络工具中的瑞士军刀,它能通过TCP和UDP在网络中读写数据。通过与其他工具结合和重定向,你可以在脚本中以多种方式使用它。使用netcat命令所能完成的事情令人惊讶。
netcat所做的就是在两台电脑之间建立链接并返回两个数据流,在这之后所能做的事就看你的想像力了。你能建立一个服务器,传输文件,与朋友聊天,传输流媒体或者用它作为其它协议的独立客户端。

其参数有以下几个:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
-d 后台模式
-e prog 程序重定向,一旦连接,就执行-e后面的程序 [危险!!]
我们可以通过shell绑定将Windows下的cmd和Linux的/bin/sh绑定,这样非常危险,我们整个系统对别人来说是完全透明,

从下面的例子中我们可以看到。这也是Linux默认去掉-e参数的原因。
-z:用来扫描机器端口 nc -z -v localhost 1-1024
-w:用来表示等待多久时间(以秒为单位)
-l:表示用来作为服务器
-p:表示端口 nc localhost 12345 -p 12346(指定本地端口为12346, 连接到localhost的12345端口)
-r:随机使用端口 nc localhost 12345 -r(随机使用可用端口),效果跟 nc localhost 12345 一样
-v:显示执行过程
-vv:更加详细的显示执行过程
-q:客户端在收到EOF时,经过多少秒才会停止,默认在收到EOF时,客户端会直接停止
-k:用在服务器端,表示当客户端连接断开时,服务器依然保持运行。默认为当客户端断开连接时,服务器端会退出.当不加该参
数时,服务器只能有一个连接,第二个客户端会连接不上,但唯一的客户大un关闭时,服务器也就关闭了加了该参数,服务

器可以接收多个连接,且当所有客户端退出时,服务器依然运行
-u:使用udp协议
-n:使用的是数字的ip地址,不加可以使用域名
-4:使用的是ipv4
-6:使用的是ipv6

nc可以用来作为远程文件复制:

将客户端的copy文件复制到服务器端

1
2
3
4
服务端:
nc -l 12345 > copy.txt
客户端:
nc localhost 12345 < copy.txt

创建远程连接

1
2
3
4
服务端:
$nc -l 1567 -e /bin/bash -i
客户端:
$nc 172.31.100.7 1567

C++向前声明

向前声明:只有class可以向前声明,struct不可以。对于向前声明,只是告诉编译器这里有一个类在外部定义,具体定义在这里不知道,所以在该文件里面,只能使用向前声明的类的指针或是应用,而不能直接定义该类的对象,因为不知道该类的具体定义。(在编译期,编译器必须知道类的大小,才能在构造相应的类实例时分配具体大小的空间。注:并非只在创建实例时才必须知道类的类型的大小,编译时,也必须知道。)对于在某些文件里面,由于只需要使用到某个类的指针或是引用,而不需要定义类的对象,此时就可以使用向前声明,而不需要包含定义该类的头文件,这样可以较少一些不必要的冲突或是错误。

volatile

volatile关键字的两层语义

一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:

1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
2)禁止进行指令重排序。
3)不保证操作原子性。

网络服务器模型简介

一个服务器一个进程

这种模型是最原始的,服务器中只有一个进程,复制监听端口,建立链接以及处理请求,然后关闭socket。这种模型效率最低,而且不能够支持长连接,对于阻塞socket来说,效率更是底下,服务器会阻塞在一个socket上,导致其他的socket被饿死。

一个链接一个进程

服务器只负责监听端口,每当收到一个链接,就fork一个进程负责处理该链接请求。这使得服务器能够同时为多个客户服务,每一个进程一个客户,客户数量的唯一限制是操作系统对以其名义运行服务器的用户ID能够同时拥有多少子进程的限制。

预先派生子进程

相比于上一个模型,预先派生子进程可以在有链接时就可以直接的有进程为客户服务,而不需要先等到fork完成后,才能够为客户服务。派生子进程在服务器启动会的时候,会预先派生一定数量的进程,当客户链接到达时,这些进程就可以立即为客户服务。所有派生的子进程都直接监听端口,单当收到连接时,就负责为建立的链接服务。由于多个子进程同时监听一个端口,会导致惊群效应。该模式只适合在于Berkeley内核的系统,对于System V, 多个进程同时在应用同一个监听socket描述符上调用accept会出现错误。

预先派生子进程+accept上锁

该模式只是上一个模式的改进,在accept上进行加锁,使得只有一个进程可以访问accept。其他的都与上一个模式一样,只不过由于加锁,这里没有了惊群效应。(其实还是有的,只不过移到了等待锁上,有多个进程在等待加锁,如果此时锁可用,那么所有等到加锁的进程都会醒过来,再一次进行加锁竞争)

预先派生子进程+传递描述符

预先派生多个进程,但是所有子进程都关闭了监听socket,只有父进程负责监听socket,父进程与子进程通过管道进行通信。有多少个子进程就有多少个管道,父进程采用select模式,监听所有的管道文件描述符和监听socket,当有新链接时,父进程负责将新的文件描述符发给空闲的子进程。父进程维护一个子进程数组,记录子进程的空闲与否,父进程与子进程通过管道进行信息交互,子进程空闲时,写管道通知父进程,同时读管道,等待父进程写新的链接描述符给子进程。当select中的管道文件描述符可读时,父进程根据管道所属的子进程来标记子进程空闲,以方便有新连接时可以将链接分配给子进程处理。

预先派线程

该系列模型其实与预先派生子进程差不多,只不过采用的是一个客户一个线程的方式。

预先派生进程或是线程都有一个问题,就是不知道开始需要派生多少,还有,需要实时对线程或是进程数量进行监控,当数量不够的时候需要采取一定的措施。

基于事件驱动

一般地,I/O多路复用机制都依赖于一个事件多路分离器(Event Demultiplexer)。分离器对象可将来自事件源的I/O事件分离出来,并分发到对应的read/write事件处理器(Event Handler)。开发人员预先注册需要处理的事件及其事件处理器(或回调函数);事件分离器负责将请求事件传递给事件处理器。两个与事件分离器有关的模式是Reactor和Proactor。Reactor模式采用同步IO,而Proactor采用异步IO。

在Reactor中,事件分离器负责等待文件描述符或socket为读写操作准备就绪,然后将就绪事件传递给对应的处理器,最后由处理器负责完成实际的读写工作。

而在Proactor模式中,处理器或者兼任处理器的事件分离器,只负责发起异步读写操作。IO操作本身由操作系统来完成。传递给操作系统的参数需要包括用户定义的数据缓冲区地址和数据大小,操作系统才能从中得到写出操作所需数据,或写入从socket读到的数据。事件分离器捕获IO操作完成事件,然后将事件传递给对应处理器。比如,在windows上,处理器发起一个异步IO操作,再由事件分离器等待IOCompletion事件。典型的异步模式实现,都建立在操作系统支持异步API的基础之上,我们将这种实现称为“系统级”异步或“真”异步,因为应用程序完全依赖操作系统执行真正的IO工作。

muduo中的TCPServer

TCPServer封装了Acceptor,并且自己提供了一个newConnection函数,作为Acceptor的连接建立回调函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
void TcpServer::newConnection(int sockfd, const InetAddress& peerAddr)
{
loop_->assertInLoopThread();
EventLoop* ioLoop = threadPool_->getNextLoop();
char buf[64];
snprintf(buf, sizeof buf, "-%s#%d", ipPort_.c_str(), nextConnId_);
++nextConnId_;
string connName = name_ + buf;

LOG_INFO << "TcpServer::newConnection [" << name_
<< "] - new connection [" << connName
<< "] from " << peerAddr.toIpPort();
InetAddress localAddr(sockets::getLocalAddr(sockfd));
// FIXME poll with zero timeout to double confirm the new connection
// FIXME use make_shared if necessary
//建立一个TCPConnection,并设置相应的回调函数
TcpConnectionPtr conn(new TcpConnection(ioLoop,
connName,
sockfd,
localAddr,
peerAddr));

connections_[connName] = conn;
conn->setConnectionCallback(connectionCallback_);
conn->setMessageCallback(messageCallback_);
conn->setWriteCompleteCallback(writeCompleteCallback_);
conn->setCloseCallback(
boost::bind(&TcpServer::removeConnection, this, _1)); // FIXME: unsafe
ioLoop->runInLoop(boost::bind(&TcpConnection::connectEstablished, conn));
}

跟TCPConnection一样,TCPServer对外提供了事件处理回调函数接口,并将这些回调函数注册到TCPConnection中。TCPServer还提供了一个ConnectionCallBack函数,该函数也是直接注册到TCPConnection中,用来在链接建立后执行,比如可以在设置高水位处理函数以及写完成处理函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
...
server_.setConnectionCallback(
boost::bind(&SudokuServer::onConnection, this, _1));
...
...
void onConnection(const TcpConnectionPtr& conn)
{
LOG_TRACE << conn->peerAddress().toIpPort() << " -> "
<< conn->localAddress().toIpPort() << " is "
<< (conn->connected() ? "UP" : "DOWN");
if (conn->connected())
{
if (tcpNoDelay_)
conn->setTcpNoDelay(true);
conn->setHighWaterMarkCallback(
boost::bind(&SudokuServer::highWaterMark, this, _1, _2), 5 * 1024 * 1024);
bool throttle = false;
conn->setContext(throttle);
}
}

TCPServer通过一个map来保存所有已经建立的链接:

1
2
typedef std::map<string, TcpConnectionPtr> ConnectionMap;
ConnectionMap connections_;

channel->TCPConnection->TCPServer每一个类有着自己的任务以及需要维护的属性,层与层之间通过回调函数来进行控制,上一层通过注册回调函数来控制下一层的事件操作以及维护自己的属性,下一层负责处理自己维护的属性以及执行上一层所注册的函数,从而实现每一层的信息维护以及上一层对下一层的操作控制。

利用注册回调函数,我们可以在不需要继承任何基类的情况下,通过TCPServer实现自己的服务器。对于不同功能的服务器,只需要提供自己的相关事件处理函数并注册到TCPServer中,就可以了!

muduo中的TCPConnection

在muduo中,socket的事件分发由channel来完成,针对不同的事件,channel负责调用不同的回调函数。TCPConnection则是对channel的封装,TCPConnection用来表示一个已建立的链接,每一个TCPConnection负责一个channel。TCPConnection属于应用层的类,拥有TCP链接的属性,如对端socket信息、链接信息等。TCPConnection有着不同事件处理函数,并且对外提供三个回调函数接口,应用可以对于不同的事件注册不同的回调函数,在TCPConnection执行完自己的事件处理流程后,会调用上一层注册的相应回调函数。TCPConnection直接将自己的事件处理函数作为回调函数注册到channel中,这样对于在channel中,实际上执行的事件处理函数为TCPConnection中的事件处理函数,用户可以通过向TCPConnection注册回调函数来控制socket不同事件所应采取的操作。

TCPConnection中有着接受和输出buffer缓冲,数据的读取都需要和buffer打交道,发送数据要比读取数据难,需要注意两个地方:

  1. 当没有数据可以发送的时候,需要关闭读事件,否则的话会造成busy loop。
  2. 当发送的数据堆积在buffer中,client一直没有接收或是数据接收很慢造成buffer太大的时候,需要进行处理,否则的话,会造成内存浪费。

TCPConnection有两个回调函数,WriteCompleteCallBackHighWaterMarkCallBack,当buffer没有数据发送时,可以执行WriteCompleteCallBack函数,当buffer太大时,可以执行HighWaterMarkCallBack函数(一般默认是直接关闭链接,回收内存),TCPConnection暴露这两个回调接口,提供该上一层注册。
TCPConnection的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
//TCPConnection维护的部分信息以及属性
EventLoop* loop_;
const string name_;
StateE state_; // FIXME: use atomic variable
// we don't expose those classes to client.
boost::scoped_ptr<Socket> socket_;
boost::scoped_ptr<Channel> channel_;
const InetAddress localAddr_;
const InetAddress peerAddr_;
//该回调函数在链接建立时执行
ConnectionCallback connectionCallback_;
MessageCallback messageCallback_;
WriteCompleteCallback writeCompleteCallback_;
HighWaterMarkCallback highWaterMarkCallback_;
CloseCallback closeCallback_;
size_t highWaterMark_;
Buffer inputBuffer_;
Buffer outputBuffer_; // FIXME: use list<Buffer> as output buffer.
boost::any context_;
bool reading_;


//建立连接
TcpConnection::TcpConnection(EventLoop* loop,
const string& nameArg,
int sockfd,
const InetAddress& localAddr,
const InetAddress& peerAddr)
: loop_(CHECK_NOTNULL(loop)),
name_(nameArg),
state_(kConnecting),
socket_(new Socket(sockfd)),
channel_(new Channel(loop, sockfd)),
localAddr_(localAddr),
peerAddr_(peerAddr),
highWaterMark_(64*1024*1024),
reading_(true)
{
channel_->setReadCallback(
boost::bind(&TcpConnection::handleRead, this, _1));
channel_->setWriteCallback(
boost::bind(&TcpConnection::handleWrite, this));
channel_->setCloseCallback(
boost::bind(&TcpConnection::handleClose, this));
channel_->setErrorCallback(
boost::bind(&TcpConnection::handleError, this));
LOG_DEBUG << "TcpConnection::ctor[" << name_ << "] at " << this
<< " fd=" << sockfd;
socket_->setKeepAlive(true);
}

void TcpConnection::connectEstablished()
{
loop_->assertInLoopThread();
assert(state_ == kConnecting);
setState(kConnected);
channel_->tie(shared_from_this());
channel_->enableReading();

connectionCallback_(shared_from_this());
}

//关闭连接
void TcpConnection::forceCloseInLoop()
{
loop_->assertInLoopThread();
if (state_ == kConnected || state_ == kDisconnecting)
{
// as if we received 0 byte in handleRead();
handleClose();
}
}

//事件处理函数

//当socket可读,但是读取数据返回0的时候,说明对端关闭了链接,此时,执行handleClose函数
void TcpConnection::handleRead(Timestamp receiveTime)
{
loop_->assertInLoopThread();
int savedErrno = 0;
ssize_t n = inputBuffer_.readFd(channel_->fd(), &savedErrno);
if (n > 0)
{
messageCallback_(shared_from_this(), &inputBuffer_, receiveTime);
}
else if (n == 0)
{

handleClose();
}
else
{
errno = savedErrno;
LOG_SYSERR << "TcpConnection::handleRead";
handleError();
}
}

void TcpConnection::handleWrite()
{
loop_->assertInLoopThread();
if (channel_->isWriting())
{
ssize_t n = sockets::write(channel_->fd(),
outputBuffer_.peek(),
outputBuffer_.readableBytes());
if (n > 0)
{
outputBuffer_.retrieve(n);
if (outputBuffer_.readableBytes() == 0)
{
channel_->disableWriting();
if (writeCompleteCallback_)
{
loop_->queueInLoop(boost::bind(writeCompleteCallback_, shared_from_this()));
}
if (state_ == kDisconnecting)
{
shutdownInLoop();
}
}
}
else
{
LOG_SYSERR << "TcpConnection::handleWrite";
// if (state_ == kDisconnecting)
// {
// shutdownInLoop();
// }
}
}
else
{
LOG_TRACE << "Connection fd = " << channel_->fd()
<< " is down, no more writing";
}
}

void TcpConnection::handleClose()
{
loop_->assertInLoopThread();
LOG_TRACE << "fd = " << channel_->fd() << " state = " << stateToString();
assert(state_ == kConnected || state_ == kDisconnecting);
// we don't close fd, leave it to dtor, so we can find leaks easily.
setState(kDisconnected);
channel_->disableAll();

TcpConnectionPtr guardThis(shared_from_this());
connectionCallback_(guardThis);
// must be the last line
closeCallback_(guardThis);
}

void TcpConnection::handleError()
{
int err = sockets::getSocketError(channel_->fd());
LOG_ERROR << "TcpConnection::handleError [" << name_
<< "] - SO_ERROR = " << err << " " << strerror_tl(err);
}

网络编程摘要

对于阻塞的socket,client和server在数据的交流时,有可能会发生死锁:

  1. client发送数据给server,server发送数据给client,如果数据都很大时,那么两端一起发送数据时,会阻塞在send操作上,由于每一个连接的接受端缓冲区的大小都是有限的,当接受端的接收缓冲区满时,发送端将一直阻塞在send操作上,这样一来,两端都没有读取接收缓冲区的数据,导致一直阻塞在send操作上。从而出现死锁。
  2. client发送很大的数据给server,server只是echo给client,由于数据比较大,client会阻塞在send上,此时echo给client的数据占满了client的接收缓冲区,那么server的send操作将会阻塞,此时不会读取client发送的数据,这样会导致server的接收缓冲区满了,此时client的send也会一直阻塞,从而出现死锁。
    这种原因是因为应用层没有缓冲,如果两端都先接收好完整的数据存在应用层的buffer上,那么就不会出现死锁了。两端先发送需要发送的数据的大小,接下来接收到完整数据后,就可以发送给对端了。

TCP 自连接

当在本地进行tcp连接时,有可能会出现自连接。所谓的自连接就是客户端和服务端共用一个port。出现这种情况的原因如下:

  1. client发起连接,连到端口为B的服务器上。
  2. 系统会选择一个port A 作为client的端口。
  3. 如果port B上面没有服务器在侦听,那么client会收到一个rst恢复包,如果有的话就可以连接上。
  4. 如果此时port A 等于 port B, 那么此时实际上是没有服务器在port B 上监听的,不然的话,client不会分配到port B。在这种情况下,client发送的SYN包会来到端口B上,此时由于client已经打开了端口B,系统会以为端口B上有服务器在监听,那么就直接接收SYN包,并发给client。这种情况其实就是client和server同时发起连接,client此时的状态为SYN_RECEIVE,按照TCP的三次握手,client会跟自己建立起了连接。此时就出现了自连接。

自连接是一种难以确定的情况,因为在TCP下,server和client并不是对等的,这种情况下分不清谁是谁了,所以会出现难以确定的问题。为了防止自连接,每当在本地连接后,可以根据client的端口和server的端口来判断是否是自连接。

先有TCP,再有UDP,一般情况下使用的是TCP,因为UDP没有拥塞控制,很难很好的使用网络的带宽,发多会丢包,发少有不能合理使用带宽。从编程角度来说,UDP不需要网络库,在服务端,udp只需要一个socket就可以与所有的客户端进行数据交互。而TCP需要一个连接一个socket。TCP是线程不安全的,因为TCP为字节流协议,多个线程操作一个socket会发生数据错乱。而UDP是线程安全的,只需要维护客户端的信息,由于是数据包的格式,所以多个线程写一个UDP socket的话,不会造成数据包内的数据错乱。

在TCP编程的时候,如果没有数据需要发送了,那么应该调用shutdownWrite,关闭写连接,在read返回0的时候,才调用close。如果在没有数据可发送的时候直接的调用close,会导致输入缓冲区的数据直接被清除,使得数据读取不到。
TCP是可靠的意思是协议会帮你把数据安全的发送到对端的接收端口,但是并不负责数据会被对端的程序给读取到,所以为了数据的可靠传输,我们在应用层需要进行数据的接收确认,表示数据已经被程序成功处理。

在linux下使用管道时,如果管道的一端关闭了,那么往里面写数据的话,程序会受到SIGPIPE信号,在默认的情况下,程序会直接退出。在TCP网络编程下,如果对端关闭了socket的读连接,此时往里面写数据的话,也会收到SIGPIPE 信号,所以在server端,需要忽略掉SIGPIPE信号,不然的话,收到SIGPIPE信号的时候,server会退出。

TCP网络编程三部曲:

  1. 开起SO_REUSEADDR,保证程序在重启的时候可以再一次的监听端口。在TCP下,如果主动关掉连接的话,会有一个TIME_WAITE时间,此时的端口还是处于忙的状态,如果不启用SO_REUSEADDR的话,绑定该端口会发生错误。
  2. 忽略SIGPIPE
  3. TCP_NODELAY, 一般情况下都是默认关掉该算法,使得数据可以立刻发送到对端。

在IO多路复用的时候,服务器监听的端口需要采用非阻塞的方式,因为如果端口可读的情况下,当真正的去accept的时候,如果在可读与真正accept之间,客户端断开了连接,那么在阻塞的情况下accept会出现阻塞。其他的连接socket也应该设置为非阻塞,不然的话,服务器会阻塞在某一个socket上,使得其他的socket被饿死。

在多路复用时,如果buffer没有数据可写或是没有数据想读,那么应该取消掉相应的时间侦听,否则的话会出现busy loop。

对于发布订阅等有多个客户端连接需要发布数据的情况,需要考虑有一些很慢的客户端的情况,极端情况下,客户端有可能不收数据,导致所有的消息都存在服务器上,造成服务器内存被消耗。所有跟多个客户端打交道的场景都需要考虑这个问题,防止个别客户端拖垮整个应用。至于如何解决,这个是应用场景需要考虑的问题,不是网络库的任务。在redis中,对于每一个客户端,会有一个buffer来缓冲服务器恢复给client的消息,如果buffer满了,redis会新开一个链表,每一个节点的空间是固定的,用来存放新到来的消息,如果链表尾节点由足够空间可以存放新消息,那么直接存储消息,否则的话需要新增一个节点,用来存放新消息,并将节点连到链表的尾部。每一次在链表存储消息后,redis会检查客户端的消息缓存的总大小,如果超过配置的阈值的话,那么直接关闭客户端。

muduo中的Channel和Poller

Channel

每一个Channel对象只负责一个文件描述符的IO时间分发,但她并不拥有这个文件描述符,也不会在析构的时候关闭该文件描述符。channel把不同的IO事件分发为不同的回调,例如readCallBack,writeCallback等,而回调用boost::function表示,用户无需继承channel。用户一般不直接使用,而回使用更上一层的封装,如TcpConnection。
channel的部分代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
namespace muduo
{
namespace net
{

class EventLoop;

class Channel : boost::noncopyable
{
public:
typedef boost::function<void()> EventCallback;
typedef boost::function<void(Timestamp)> ReadEventCallback;

Channel(EventLoop* loop, int fd);
~Channel();

void handleEvent(Timestamp receiveTime);
void setReadCallback(const ReadEventCallback& cb)
{ readCallback_ = cb; }

void setWriteCallback(const EventCallback& cb)
{ writeCallback_ = cb; }

void setCloseCallback(const EventCallback& cb)
{ closeCallback_ = cb; }

void setErrorCallback(const EventCallback& cb)
{ errorCallback_ = cb; }


void enableReading() { events_ |= kReadEvent; update(); }
void disableReading() { events_ &= ~kReadEvent; update(); }
void enableWriting() { events_ |= kWriteEvent; update(); }
void disableWriting() { events_ &= ~kWriteEvent; update(); }
void disableAll() { events_ = kNoneEvent; update(); }
bool isWriting() const { return events_ & kWriteEvent; }
bool isReading() const { return events_ & kReadEvent; }

private:

EventLoop* loop_;
const int fd_;
int events_;
int revents_; // it's the received event types of epoll or poll
int index_; // used by Poller.

ReadEventCallback readCallback_;
EventCallback writeCallback_;
EventCallback closeCallback_;
EventCallback errorCallback_;
};

}
}
//revents_由poller来设置,根据触发的事件类型来执行相应的回调
void Channel::handleEventWithGuard(Timestamp receiveTime)
{
eventHandling_ = true;
LOG_TRACE << reventsToString();
if ((revents_ & POLLHUP) && !(revents_ & POLLIN))
{
if (logHup_)
{
LOG_WARN << "fd = " << fd_ << " Channel::handle_event() POLLHUP";
}
if (closeCallback_) closeCallback_();
}

if (revents_ & POLLNVAL)
{
LOG_WARN << "fd = " << fd_ << " Channel::handle_event() POLLNVAL";
}

if (revents_ & (POLLERR | POLLNVAL))
{
if (errorCallback_) errorCallback_();
}
if (revents_ & (POLLIN | POLLPRI | POLLRDHUP))
{
if (readCallback_) readCallback_(receiveTime);
}
if (revents_ & POLLOUT)
{
if (writeCallback_) writeCallback_();
}
eventHandling_ = false;
}

Poller

Poller calss为IO multiplexing的封装。Poller并不拥有channel,channel在析构前必须自己unregister(EventLoop::removeChannel()),避免空悬指针。
Poller使用两个数据结构来记录需要监听的channel:

1
2
3
4
typedef std::map<int, Channel*> ChannelMap;
typedef std::vector<struct pollfd> PollFdList;
ChannelMap channels_;
PollFdList pollfds_;

pollfds_用来记录需要监听的文件描述符以及对应监听的事件,每一个channel在这里面都有一个元素与之对应,元素的下标就是channel成员变量中的index_,poller可以跟根据每一个channel中的index来对pollfds_进行更新(removeChannel, updateChannel等操作)。

channels_用来记录每一个文件描述符对应的channel指针,这里的key为channel的文件描述符,不可以为channel的index_,因为index_会改变。当Poller获得文件描述符的监听事件后,就根据channels_来获得有时间发生的文件描述符的channel,并设置对应channel的revents_(实际发生的事件),接着将每一个有事件的channel放进一个vector里面,最后将vector返回给EventLoop,作为activateChannels。

之所以需要返回activateChannels,而不是在遍历发生事件的文件描述符的同时执行对应的channel中的handleEvent函数,是因为handleEvent函数有可能会对pollfds_进行修改,如删除channel,使得在迭代期间pollfds_大小发生改变,这是一件很危险的事情。另一个原因就是简化Poller的职责,Poller只负责监听,不负责事件的分发与处理。

如果某一个channel暂时不关心任何事件,那么可以吧pollfd.fd设置为负数,这样poll会忽略此文件描述符。不能将pollfd.events设置为0,因为无法屏蔽POLLERR事件。muduo的改进做法是吧pollfd.fd设为channel->fd()的相反数减一(文件描述符从0开始,减一是为了兼容0,因为0的相反数还是0)。

但需要删除channel时,由于pollfds_使用的是vector来存储channel的,可以根据channle本身的index_来确定该channel在vector中所在的位置。如果直接删除该位置的话,那么后面的channel都需要上移一个位置,导致后面的channel都需要更新自己的index_,这样效率会很低。这里有一个小技巧,就是直接将vector最后的元素A与该位置进行呼唤,只需要修改A的index_就好,其他的channel不受影响。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
void PollPoller::removeChannel(Channel* channel)
{
Poller::assertInLoopThread();
LOG_TRACE << "fd = " << channel->fd();
assert(channels_.find(channel->fd()) != channels_.end());
assert(channels_[channel->fd()] == channel);
assert(channel->isNoneEvent());
int idx = channel->index();
assert(0 <= idx && idx < static_cast<int>(pollfds_.size()));
const struct pollfd& pfd = pollfds_[idx]; (void)pfd;
assert(pfd.fd == -channel->fd()-1 && pfd.events == channel->events());
size_t n = channels_.erase(channel->fd());
assert(n == 1); (void)n;
if (implicit_cast<size_t>(idx) == pollfds_.size()-1)
{
pollfds_.pop_back();
}
else
{
int channelAtEnd = pollfds_.back().fd;
iter_swap(pollfds_.begin()+idx, pollfds_.end()-1);
if (channelAtEnd < 0)
{
channelAtEnd = -channelAtEnd-1;
}
channels_[channelAtEnd]->set_index(idx);
pollfds_.pop_back();
}
}

muduo中的EventLoop

muduo主要采用Reactor模式来实现C++网络库。一个EventLoop对应一个线程。服务器就是在EventLoop中来实现对网络连接的管理的,所有的socket的读写都在该loop中实现。EventLoop的执行大体步骤如下:

EventLoop执行流程图

代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
void EventLoop::loop()
{
assert(!looping_);
assertInLoopThread();
looping_ = true;
quit_ = false; // FIXME: what if someone calls quit() before loop() ?
LOG_TRACE << "EventLoop " << this << " start looping";

while (!quit_)
{
activeChannels_.clear();
pollReturnTime_ = poller_->poll(kPollTimeMs, &activeChannels_);
++iteration_;
if (Logger::logLevel() <= Logger::TRACE)
{
printActiveChannels();
}
// TODO sort channel by priority
eventHandling_ = true;
for (ChannelList::iterator it = activeChannels_.begin();
it != activeChannels_.end(); ++it)
{
currentActiveChannel_ = *it;
currentActiveChannel_->handleEvent(pollReturnTime_);
}
currentActiveChannel_ = NULL;
eventHandling_ = false;
doPendingFunctors();
}

LOG_TRACE << "EventLoop " << this << " stop looping";
looping_ = false;
}

pendingFunctors的定义如下:

1
2
typedef boost::function<void()> Functor;
std::vector<Functor> pendingFunctors_;

在EventLoop中,只有pendingFunctors暴露给其他的线程,所以对于该成员的修改需要mutex来保护

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void EventLoop::runInLoop(Functor&& cb)
{
if (isInLoopThread())
{
cb();
}
else
{
queueInLoop(std::move(cb));
}
}

void EventLoop::queueInLoop(Functor&& cb)
{
{
MutexLockGuard lock(mutex_);
pendingFunctors_.push_back(std::move(cb)); // emplace_back
}

if (!isInLoopThread() || callingPendingFunctors_)
{
wakeup();
}
}

在遍历pendingFunctors的时候,采用的是先剪切,再遍历:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void EventLoop::doPendingFunctors()
{
std::vector<Functor> functors;
callingPendingFunctors_ = true;

{
MutexLockGuard lock(mutex_);
functors.swap(pendingFunctors_);
}

for (size_t i = 0; i < functors.size(); ++i)
{
functors[i]();
}
callingPendingFunctors_ = false;
}

这样可以在遍历的时候,不会阻塞其他线程往EventLoop里面注册函数,并且,由于回调函数里面有可能也会调用queueInLoop函数,所以这样也可以避免死循环,先剪切后,遍历的是新的functors,原来的pendingfunctors为空,如果回调函数又执行queueInLoop的话,注册的是pendingFunctors,跟新的Functors无关。所以可以安全的遍历新的Functors。

由于EventLoop有可能会一直阻塞在poll中,此时如果有回调函数需要EventLoop马上执行的话,那么需要立马唤醒EventLoop的阻塞,为了实现该功能,EventLoop使用了Linux现有的eventfd。

eventfd:实现了线程之间事件通知的方式,eventfd的缓冲区大小是sizeof(uint64_t);向其write可以递增这个计数器,read操作可以读取,并进行清零;eventfd也可以放到监听队列中,当计数器不是0时,有可读事件发生,可以进行读取。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
EventLoop::EventLoop()
: looping_(false),
quit_(false),
eventHandling_(false),
callingPendingFunctors_(false),
iteration_(0),
threadId_(CurrentThread::tid()),
poller_(Poller::newDefaultPoller(this)),
timerQueue_(new TimerQueue(this)),
wakeupFd_(createEventfd()),
wakeupChannel_(new Channel(this, wakeupFd_)),
currentActiveChannel_(NULL)
{
LOG_DEBUG << "EventLoop created " << this << " in thread " << threadId_;
if (t_loopInThisThread)
{
LOG_FATAL << "Another EventLoop " << t_loopInThisThread
<< " exists in this thread " << threadId_;
}
else
{
t_loopInThisThread = this;
}
wakeupChannel_->setReadCallback(
boost::bind(&EventLoop::handleRead, this));
// we are always reading the wakeupfd
wakeupChannel_->enableReading();
}

int createEventfd()
{

int evtfd = ::eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC);
if (evtfd < 0)
{
LOG_SYSERR << "Failed in eventfd";
abort();
}
return evtfd;
}

void EventLoop::wakeup()
{
uint64_t one = 1;
ssize_t n = sockets::write(wakeupFd_, &one, sizeof one);
if (n != sizeof one)
{
LOG_ERROR << "EventLoop::wakeup() writes " << n << " bytes instead of 8";
}
}

void EventLoop::handleRead()
{
uint64_t one = 1;
ssize_t n = sockets::read(wakeupFd_, &one, sizeof one);
if (n != sizeof one)
{
LOG_ERROR << "EventLoop::handleRead() reads " << n << " bytes instead of 8";
}
}

EventLoop提供一个接口来直接退出EventLoop循环,该接口为public,允许外面的对象访问:

1
2
3
4
5
6
7
8
9
10
11
void EventLoop::quit()
{
quit_ = true;
// There is a chance that loop() just executes while(!quit_) and exits,
// then EventLoop destructs, then we are accessing an invalid object.
// Can be fixed using mutex_ in both places.
if (!isInLoopThread())
{
wakeup();
}
}

muduo中的buffer

muduo在应用层上加了一个缓存机制,用户发送接受数据只需要与buffer打交道就好,底层的网络通信由buffer来实现。

为什么non-blocking网络编程中应用层buffer是必须的

non-blocking IO的核心思想就是避免阻塞在read()或write()或其他的IO系统调用上,这样可以最大限度的复用thread-of-control,让一个线程能服务于多个sockets链接。IO线程只能阻塞在IO multiplexing函数上,如select/poll/epoll_wait.这样一来,应用层的缓存是必须的。每一个TCP socket都要有stateful的input buffer和output buffer。

TCPConnection必须要有output buffer

考虑一个常见的场景:程序想要通过TCP联系发送100KB的数据,但是write()调用中,操作系统只接受80KB的,此时如果直接让应用程序发送的话,那么剩下的20KB数据就发不出去,应用程序可以等到发送缓冲区有空间的时候继续发送,但是不知道什么时候有空间,此时将会阻塞,如果不等待的话,应用程序需要记录剩下的20KB,等到下一次发,然后继续循环上面的操作。但是应用程序只是负责生产数据,对于数据什么时候发送,以怎样的大小发送,应用程序并不关心,这些事情应该都由网络库来实现,所以需要有output buffer。

TcpConnection必须要有input buffer

TCP是一个无边界的字节流协议,接受方必须要处理“收到的数据尚不构成一条完整的消息”和“一次性收到两条消息的数据”等情况。网络库在处理socket可读事件的时候,必须一次性吧socket里的数据读完,否则会反复触发POLLIN事件,造成busy-loop。那么网络库就需要应对数据不完整的情况,需要将收到的数据放在input buffer中,等构成一条完整的消息后,在通知应用程序。也就是应用层的消息分包

Buffer的类图

buffer内部数据存储结构为vector,支持类似于队列的先进先出操作。具体的类图如下:

Buffer::readFD()

在非阻塞网络编程中,对于缓冲去大小的设定需要根据不同应用的场景来决定。为了减少系统调用,一次性读的数据越多约好,但是需要一个比较大的缓冲区。另一方面,为了减少系统的内存占用,应该将缓冲区设的小一点比较好,不然的话,如果一个连接的缓冲区大小为1M,那么1000个连接就需要21G的内存,但是在大多数情况下,buffer的使用率很低,此时大的buffer会造成内存的浪费。

在muduo里面,readFD函数的实现为:在栈上准备一个65536字节的extrabuf,然后利用readv()来读取数据,iovec有两块,一个指向input buffer的writable字节,一个指向extrabuf。如果读入的数据很多,那么可以放在extrabuf里面,然后程序再把extrabuf里面的数据append到buffer中,如果读取数据比较少,那么数据直接存在buffer里面,不需要额外的操作。

buffer数据结构

prependable = readIndex
readable = writeIndex-readIndex
writable = size()-writeIndex

数据结构的初始化如下:

在数据的读取过程中,readIndex和writeIndex随着读取操作的进行而改变。
readIndex和writeIndex随着读取的进行而改变

自动增长和内部腾挪

当当前的可写空间不够的时候,那么需要分配一个更大的内存,在分配好内存进行数据的复制时,顺便进行碎片回收,将所有数据负载到新内存的kCheapPrependable个字节后面。由于vector重新分配了内存,原有的指向其元素的指针会失效,所以buffer中采用的是readIndex和writeIndex来表示可读和可写的其实下标。

当前的内存状态

写入1000字节后,内存的状态

具体的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void makeSpace(size_t len)
{
if (writableBytes() + prependableBytes() < len + kCheapPrepend)
{
// FIXME: move readable data
buffer_.resize(writerIndex_+len);
}

else
{
// move readable data to the front, make space inside buffer
assert(kCheapPrepend < readerIndex_);

size_t readable = readableBytes();
std::copy(begin()+readerIndex_,
begin()+writerIndex_,
begin()+kCheapPrepend);
readerIndex_ = kCheapPrepend;

writerIndex_ = readerIndex_ + readable;
assert(readable == readableBytes());
}
}

前方添加

prependable的作用就是实现高效率的前方添加。预留一定的小空间,但需要在数据的前面添加数据时,就可以直接的添加了,否则的话,需要对整个buffer进行操作。
例如:程序以固定的4字节表示数据的长度,但需要序列化一个消息的时候,不知道该消息的长度,那么可以一直append直到序列化完成为止,然后就可以在前面添加最终的长度。

多线程与fork()

fork一般不能在多线程程序中调用,因为Linux的fork()只克隆当前线程的thread of control,不克隆其他的线程。fork()之后,除了当前的线程之外,其他的线程都消失。也就是说fork不能一下子得到一个与父进程一样的多线程子进程。

fork()之后,子进程中已有一个线程,其他的线程都消失了,这就造成一个危险的局面。在执行fork前,有一个mutex被线程锁住了,此时调用fork,子进程空间的mutex会处于锁住状态,由于父进程中的mutex与子进程中的muetx属于不同的空间,即使父进程中的mutex别解锁了,此时子进程中的mutex也是处于锁住状态,在子进程中根本就没有谁拥有该锁,这就导致子进程的mutex一直处于锁住状态,此时如果子进程对mutex加锁的话,那么就会进入死锁。

Linux上的线程标识

POSIX threads库提供了pthread_self接口,可以获得进程里面的线程id,其类型为pthread_t类型。pthread_t不一定是一个数值类型,也有可能是一个结构体,它是一个非可移植性的类型,也就是说,在这个系统中,可能是unsigned int类型,在别的系统可能是long,double,或者甚至就是个结构体,都不得而知,所以非可移植。因此Pthreads专门提供了pthread_equal函数用于比较两个线程标识符是否相等。这就带来了一系列问题:

  1. 无法打印输出pthread_t,因为不知道其正真的类型。
  2. 无法比较pthread_t的大小或是hash值,所以不能作为关联容器的key
  3. pthread_t只是在进程内有意义,与操作系统的任务调度之间无法建立有效的关联。在/proc文件系统中找不到pthread_t对应的task。

因此,pthread_t并不适合作为程序中对线程的标识符。

在Linux上,可以使用gettid系统调用来获得线程的id,这么做的好处为:

  1. 他的类型为pid_t, 其值通常是一个小的整数型,便于在日志中输出
  2. 在现代的Linux操作系统中,她直接表示内核的任务调度id,因此在/proc文件系统中可以轻易找到其对应项
  3. 在其他的系统工具中容易定位到具体的线程。例如在top命令中,可以按照线程列出任务,可以根据CPU或是内存的使用率定位到具体的线程
  4. 任何时刻都是全局唯一的。
  5. 0为非法值,因为操作系统第一个进程init的pid为1。

如何获取线程的TID(thread ID)?

通过查看man得到如下描述:

(1) The gettid() system call first appeared on Linux in kernel 2.4.11.
(2) gettid() returns the thread ID of the current process. This is equal to the process ID (as returned by getpid(2)),
unless the process is part of a thread group (created by specifying the CLONE_THREAD flag to the clone(2) system call). All processes in the same thread group have the same PID, but each one has a unique TID.
(3) gettid() is Linux specific and should not be used in programs that are intended to be portable.
(如果考虑移植性,不应该使用此接口)

1
2
3
4
5
#include <sys/syscall.h>  
#define gettidv1() syscall(__NR_gettid)
#define gettidv2() syscall(SYS_gettid)
printf("The ID of this thread is: %ld\n", (long int)gettidv1());// 最新的方式
printf("The ID of this thread is: %ld\n", (long int)gettidv2());// 传统方式

C++中线程安全的单例实现

单例模式的实现理论上采用double checked locking 不会出现线程安全问题,但是有可能编译器会对程序进行优化,使得程序乱序执行,导致出现错误。在Java中,可以借助类的转载阶段初始化静态区域来避免该问题,但是C++没有这个机制。在C++的单例实现中,可以使用pthread_once()函数来实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/*
*在Linux thread中,一次性函数的执行有三种状态:NEVER(0), IN_PROGRESS(1), DONE(3),
*处于IN_PROGRESS状态的话,所有所有调用 pthrad_once函数都会等待“已执行一次信号”才会退出阻塞,
*对于处于DONE的话,所有pthread_once函数都会立刻返回0.所以_ponce的初始值
*应该为PTHREAD_ONCE_INIT(值为0)
*/

template<typename T>
class Singleton: boost::nocopyable{
public:
static T& instance(){
pthread_once(&ponce_, &Singleton::init);
return *value_;
}

private:
Singleton();
~Singleton();
static void init(){
value_ = new T(); //这里一定要使用new,使得对象存在对中,不会随着函数的退出而消亡
}

//静态成员变量只能在类外定义
private:
static pthread_once_t ponce_;
static T* value_;
};

//必须在头文件里面定义static变量
template<typename T>
pthread_once_t Singleton<T>::ponce_ = PTHREAD_ONCE_INIT;

template<typename T>
T* Singleton<T>::value_ = nullptr;

条件变量的使用

条件变量的正确使用方式一般为:

  1. 必须与mutex一起使用,该布尔表达式的读写需要受到mutex的保护
  2. 在mutex已上锁的时候才能用 wait()
  3. 吧判断布尔条件和wait()放到while循环中

写成代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
mudo::MutexLock mutex;
mudo::Condition cond(mutex);
std::dequeue<int> queue;

int dequeue(){
MutexLockGuard lock(mutex);
while(queue.empty()){
cond.wait(); //会原子的unlock mutex进入等待,不会与enqueue形成死锁,当wait出来后,会自动加锁
}
assert(!queue.empty());
int top = queue.front();
queue.pop_front();
return top;
}

使用while循环来等待条件变量,而不能使用if的原因就是在linux下有时会出现虚假唤醒,在多核的情况下,一个noify有可能会唤醒多个wait函数,造成虚假唤醒。还有一个原因就是,如果使用if语句的话,那么有可能在enqueue的时候不小心使用了broadcast,造成所有等待wait的线程被唤醒,每一个线程都加锁后进行pop操作,就有可能导致队列为空的情况下还在pop,从而出现错误。

对于signal和broadcast端:

  1. 在signal之前一定要修改布尔表达式
  2. 修改布尔表达式要在mutex的保护下
  3. 注意区分signal和broadcast:broadcast通常用于表明状态变化,signal通常用于表示资源可用

代码为:

1
2
3
4
5
void enqueue(int x){
MutexLockGuard lock(mutex);
queue.push_back(x);
cond.notify() //notify可以移出临界区之外
}

在上面的代码中,enqueue每一次push都会调用notify,能不能在队列从0变为1的时候才调用notify?

对于只有一个dequeue的线程的话可以这么做,但对于有多个dequeue的话,就不行。因为只有在0变1
的情况下调用notify,此时只有一个线程被唤醒,即使队列有很多资源可以使用,其他线程也会一直
阻塞在wait里面。造成线程浪费,程序并发性得不到应用。

LCA之DFS+ST算法

DFS+ST算法可以在线的查询两个节点的最近公共祖先。由于任意两点的最近公共祖先必定在这两点的最短路径上,所以也可以根据这个特性来查找任意两点的最近路径。
DFS:深度优先遍历树的每一个节点,用数组deep按照遍历顺序记录下每一个节点的深度,另外需要记录每一个节点在数组中所对应的第一个出现的下标。
ST:有了深度数组,利用ST算法计算每一个偶数段的区间的最小深度的下标。
对于每一个查询(a, b), 先获得a和b在深度数组中第一次出现的下标firsta和firstb,接下来就是求出在深度数组中下标从firsta到firstb的最小值的下标,改下标对应的节点就是最近公共祖先。求最小值的下标ST算法可以在线的以常数的时间给出。如果需要求出两点之前的最短路径长度,那么可以直接的deep[a]+deep[b]-2*deep[最近公共祖先下标]。

例题:

采用DFS遍历整棵树,得到以下数据:

(1)遍历序列p:0  1  3  1  4  7  4  8  4  1  5  1  0  2  6  2  0

(2)各节点的深度序列        depth: 0  1   1   2   2  2   2   3  3

(3)各节点在序列p中首次出现的位置序列pos: 0  1  13  2  4  10  14  5  7

使用ST算法,假设现在我们要求节点7和5的最短路径,我们可以这样做:

(1)首先,从pos序列中获得节点7和节点5在p序列中第一次出现的位置分别为:pos[7] = 5, pos[5] = 10;

(2)得到p序列中[5, 10]这一段子序列s:7  4  8  4  1  5

(3)s序列中深度最小的点即节点1就是我们要找的节点7和节点5的LCA。

例子:最近公共祖先·三

描述

上上回说到,小Hi和小Ho使用了Tarjan算法来优化了他们的“最近公共祖先”网站,但是很快这样一个离线算法就出现了问题:如果只有一个人提出了询问,那么小Hi和小Ho很难决定到底是针对这个询问就直接进行计算还是等待一定数量的询问一起计算。毕竟无论是一个询问还是很多个询问,使用离线算法都是只需要做一次深度优先搜索就可以了的。

那么问题就来了,如果每次计算都只针对一个询问进行的话,那么这样的算法事实上还不如使用最开始的朴素算法呢!但是如果每次要等上很多人一起的话,因为说不准什么时候才能够凑够人——所以事实上有可能要等上很久很久才能够进行一次计算,实际上也是很慢的!

“那到底要怎么办呢?在等到10分钟,或者凑够一定数量的人两个条件满足一个时就进行运算?”小Ho想出了一个折衷的办法。

“哪有这么麻烦!别忘了和离线算法相对应的可是有一个叫做在线算法的东西呢!”小Hi笑道。

小Ho面临的问题还是和之前一样:假设现在小Ho现在知道了N对父子关系——父亲和儿子的名字,并且这N对父子关系中涉及的所有人都拥有一个共同的祖先(这个祖先出现在这N对父子关系中),他需要对于小Hi的若干次提问——每次提问为两个人的名字(这两个人的名字在之前的父子关系中出现过),告诉小Hi这两个人的所有共同祖先中辈分最低的一个是谁?

提示:最近公共祖先无非就是两点连通路径上高度最小的点嘛!

输入

每个测试点(输入文件)有且仅有一组测试数据。

每组测试数据的第1行为一个整数N,意义如前文所述。

每组测试数据的第2~N+1行,每行分别描述一对父子关系,其中第i+1行为两个由大小写字母组成的字符串Father_i, Son_i,分别表示父亲的名字和儿子的名字。

每组测试数据的第N+2行为一个整数M,表示小Hi总共询问的次数。

每组测试数据的第N+3~N+M+2行,每行分别描述一个询问,其中第N+i+2行为两个由大小写字母组成的字符串Name1_i, Name2_i,分别表示小Hi询问中的两个名字。

对于100%的数据,满足N<=10^5,M<=10^5, 且数据中所有涉及的人物中不存在两个名字相同的人(即姓名唯一的确定了一个人),所有询问中出现过的名字均在之前所描述的N对父子关系中出现过,且每个输入文件中第一个出现的名字所确定的人是其他所有人的公共祖先。

输出

对于每组测试数据,对于每个小Hi的询问,按照在输入中出现的顺序,各输出一行,表示查询的结果:他们的所有共同祖先中辈分最低的一个人的名字。

样例输入

4
Adam Sam
Sam Joey
Sam Micheal
Adam Kevin
3
Sam Sam
Adam Sam
Micheal Kevin

样例输出

Sam
Adam
Adam

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
#include <iostream>
#include <string>
#include <vector>
#include <queue>
#include <map>
#include <algorithm>

using namespace std;

const int maxn = 200009;
const int maxl = 21;

map <string, int> rn;
vector <int> e[maxn];
string name[maxn];
int n, m, tn, ath[maxn][maxl], d[maxn];

int getNum(string n) {
map <string, int> :: iterator i = rn. find(n);
if (i == rn. end()) {
name[++ tn] = n;
rn. insert(pair<string, int> (n, tn));
return tn;
}
else
return i-> second;
}

void buildTree() {
queue <int> q;
int p0 = 1;
while (ath[p0][0])
p0 = ath[p0][0];
d[p0] = 1;
q. push(p0);
while (!q. empty()) {
int p = q. front();
q. pop();
for (int i = 1; i < maxl; ++ i)
ath[p][i] = ath[ath[p][i - 1]][i - 1];
for (vector <int> :: iterator i = e[p]. begin(); i != e[p]. end(); ++ i) {
q. push(*i);
d[*i] = d[p] + 1;
}
}
}

int LCA(int p, int q) {
if (d[p] < d[q])
swap(p, q);
for (int i = maxl - 1; i >= 0; -- i)
if (d[ath[p][i]] >= d[q])
p = ath[p][i];
if (p == q)
return p;
for (int i = maxl - 1; i >= 0; -- i)
if (ath[p][i] ^ ath[q][i])
p = ath[p][i], q = ath[q][i];
return ath[p][0];
}

int main() {
cin. sync_with_stdio(0);
cin >> n;
tn = 0;
while (n --) {
string a, b;
cin >> a >> b;
int na = getNum(a), nb = getNum(b);
ath[nb][0] = na;
e[na]. push_back(nb);
}
buildTree();
cin >> m;
while (m --) {
string a, b;
cin >> a >> b;
int na = getNum(a), nb = getNum(b);
cout << name[LCA(na, nb)] << endl;
}
}

代码来自hiho一下 第十七周的排名第一参赛者laekov

LCA之Tarjan算法

寻找最近公共祖先有两种效率高的算法,一种是离线算法Tarjan算法,用户输入所有查询,算法给出所有查询结果,算法在运行的时候就已经知道用户的所有查询了,时间复杂度为O(nlogn)。另外一种是在线算法DFS+ST算法,算法运行不需要知道用户的输入,每一次查询可以在常数时间内给出结果。时间复杂度为DFS->O(n), ST->O(nlogn)。这里主要讲一下Tarjan算法。

Tarjan算法是DFS搜索和并查集的思想的结合,深度搜索每一个节点,对于当前的节点x,将其看做一个新的集合,并将其作为该集合的代表,查询处理与该节点有关的查询,接着递归遍历该节点的所有子节点,在遍历完所有子节点后,将该集合与root集合合并,也就是并查集的Union操作。在查询的过程中还可以使用路径压缩的方法来提高查询效率。例如:如果有查询(a, b),那么当遍历到a节点的时候,如果b节点已经遍历过了,那么分两种情况,如果b在另外一个子树上,那么查找b的时候,b所在的结合的代表就是a和b的最近公共祖先。如果b不在另一颗子树上,而是与a同在一颗子树,那么此时的b就是最近公共祖先。这也就是为什么在需要在遍历完节点的所有子节点后才可以将当前节点与root合并的原因。

例子:Nearest Common Ancestors

Description

A rooted tree is a well-known data structure in computer science and engineering. An example is shown below:

In the figure, each node is labeled with an integer from {1, 2,…,16}. Node 8 is the root of the tree. Node x is an ancestor of node y if node x is in the path between the root and node y. For example, node 4 is an ancestor of node 16. Node 10 is also an ancestor of node 16. As a matter of fact, nodes 8, 4, 10, and 16 are the ancestors of node 16. Remember that a node is an ancestor of itself. Nodes 8, 4, 6, and 7 are the ancestors of node 7. A node x is called a common ancestor of two different nodes y and z if node x is an ancestor of node y and an ancestor of node z. Thus, nodes 8 and 4 are the common ancestors of nodes 16 and 7. A node x is called the nearest common ancestor of nodes y and z if x is a common ancestor of y and z and nearest to y and z among their common ancestors. Hence, the nearest common ancestor of nodes 16 and 7 is node 4. Node 4 is nearer to nodes 16 and 7 than node 8 is.

For other examples, the nearest common ancestor of nodes 2 and 3 is node 10, the nearest common ancestor of nodes 6 and 13 is node 8, and the nearest common ancestor of nodes 4 and 12 is node 4. In the last example, if y is an ancestor of z, then the nearest common ancestor of y and z is y.

Write a program that finds the nearest common ancestor of two distinct nodes in a tree.

Input

The input consists of T test cases. The number of test cases (T) is given in the first line of the input file. Each test case starts with a line containing an integer N , the number of nodes in a tree, 2<=N<=10,000. The nodes are labeled with integers 1, 2,…, N. Each of the next N -1 lines contains a pair of integers that represent an edge –the first integer is the parent node of the second integer. Note that a tree with N nodes has exactly N - 1 edges. The last line of each test case contains two distinct integers whose nearest common ancestor is to be computed.
Output

Print exactly one line for each test case. The line should contain the integer that is the nearest common ancestor.

Sample Input

2
16
1 14
8 5
10 16
5 9
4 6
8 4
4 10
1 13
6 15
10 11
6 7
10 2
16 3
8 1
16 12
16 7
5
2 3
3 4
3 1
1 5
3 5

Sample Output

4
3

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
#include<stdio.h>
#include<vector>
#include<string.h>
using namespace std;
#define Size 11111 //节点个数

vector<int> node[Size],que[Size];
int n,pare[Size],anse[Size],in[Size],rank[Size];

int vis[Size];
void init()
{

int i;
for(i=1;i<=n;i++)
{
node[i].clear();
que[i].clear();
rank[i]=1;
pare[i]=i;///
}
memset(vis,0,sizeof(vis));
memset(in,0,sizeof(in));
memset(anse,0,sizeof(anse));

}

int find(int nd)
{

return pare[nd]==nd?nd:pare[nd]=find(pare[nd]);
}
int Union(int nd1,int nd2)
{

int a=find(nd1);
int b=find(nd2);
if(a==b) return 0;
else if(rank[a]<=rank[b])
{
pare[a]=b;
rank[b]+=rank[a];
}
else
{
pare[b]=a;
rank[a]+=rank[b];
}
return 1;

}

void LCA(int root)
{

int i,sz;
anse[root]=root;//首先自成一个集合
sz=node[root].size();
for(i=0;i<sz;i++)
{
LCA(node[root][i]);//递归子树
Union(root,node[root][i]);//将子树和root并到一块
anse[find(node[root][i])]=root;//修改子树的祖先也指向root
}
vis[root]=1;
sz=que[root].size();
for(i=0;i<sz;i++){
if(vis[que[root][i]]){

//root和que[root][i]所表示的值的最近公共祖先
printf("%d\n",anse[find(que[root][i])]);
return ;
}
}
return ;
}

int main()
{

int cas,i;
scanf("%d",&cas);
while(cas--)
{
int s,e;
scanf("%d",&n);
init();
for(i=0;i<n-1;i++)
{
scanf("%d %d",&s,&e);
if(s!=e)
{
node[s].push_back(e);
// node[e].push_back(s);
in[e]++;
}
}
scanf("%d %d",&s,&e);
que[s].push_back(e);
que[e].push_back(s);
for(i=1;i<=n;i++) if(in[i]==0) break;//寻找根节点
// printf("root=%d\n",i);
LCA(i);
}
return 0;
}

代码转载自这里

例子:CD操作

Problem Description

在Windows下我们可以通过cmd运行DOS的部分功能,其中CD是一条很有意思的命令,通过CD操作,我们可以改变当前目录。
这里我们简化一下问题,假设只有一个根目录,CD操作也只有两种方式:   

  1. CD 当前目录名...\目标目录名 (中间可以包含若干目录,保证目标目录通过绝对路径可达)
  2. CD .. (返回当前目录的上级目录)
      
    现在给出当前目录和一个目标目录,请问最少需要几次CD操作才能将当前目录变成目标目录?
Input

输入数据第一行包含一个整数T(T<=20),表示样例个数;
每个样例首先一行是两个整数N和M(1<=N,M<=100000),表示有N个目录和M个询问;
接下来N-1行每行两个目录名A B(目录名是只含有数字或字母,长度小于40的字符串),表示A的父目录是B。
最后M行每行两个目录名A B,表示询问将当前目录从A变成B最少要多少次CD操作。
数据保证合法,一定存在一个根目录,每个目录都能从根目录访问到。

Output

请输出每次询问的结果,每个查询的输出占一行。

Sample Input

2
3 1
B A
C A
B C

3 2
B A
C B
A C
C A

Sample Output

2
1
2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
//思路:

//  求出a和b的最近公共祖先,然后分4种情况讨论

//  ①. a和b有一个公共祖先c,则用 c时间戳-a的时间戳+1(1步可以直接从c到b)

//  ②. a是b的祖先,则只用1步就可以到达b点

//  ③. b是a的祖先,则用a的时间戳-b的时间戳

//  ④. a和b是同一个点,则答案是0

#include<stdio.h>
#include<vector>
#include<string.h>
#include<map>
#include<math.h>
#include<string>
using namespace std;
#define Size 111111 //节点个数
struct Query
{
int nd,id;
}temp;
struct out
{
int s,e;
}out[Size];
vector<int> node[Size];
vector<struct Query>que[Size];
int n,m,pare[Size],ance[Size],in[Size],rank[Size],dis[Size],ans[Size],vis[Size];
map<string,int>mp;
void init()
{

int i;
for(i=1;i<=n;i++)
{
node[i].clear();
que[i].clear();
rank[i]=1;
pare[i]=i;///
}
memset(vis,0,sizeof(vis));
memset(in,0,sizeof(in));
memset(ance,0,sizeof(ance));
memset(dis,0,sizeof(dis));
mp.clear();
}
int aabs(int aa)
{

if(aa>0) return aa;
else return -aa;
}
int find(int nd)//并查集操作 不解释
{

return pare[nd]==nd?nd:pare[nd]=find(pare[nd]);
}
int Union(int nd1,int nd2)//并查集操作 不解释
{

int a=find(nd1);
int b=find(nd2);
if(a==b) return 0;
else if(rank[a]<=rank[b])
{
pare[a]=b;
rank[b]+=rank[a];
}
else
{
pare[b]=a;
rank[a]+=rank[b];
}
return 1;
}
void LCA(int root,int num)
{

int i,sz;
ance[root]=root;//首先自成一个集合
dis[root]=num;
sz=node[root].size();
for(i=0;i<sz;i++)
{
LCA(node[root][i],num+1);//递归子树
Union(root,node[root][i]);//将子树和root并到一块
ance[find(node[root][i])]=root;//修改子树的祖先也指向root
}
vis[root]=1;
sz=que[root].size();
for(i=0;i<sz;i++)
{
int nd1,nd2,idx,ancestor;
nd1=root;nd2=que[root][i].nd;idx=que[root][i].id;
if(vis[nd2])
{
ans[idx]=ance[find(nd2)];
}
}
return ;
}

int main()
{

int cas,i;
scanf("%d",&cas);
while(cas--)
{
char ss[100],ee[100];
int s,e,cnt=1;
scanf("%d %d",&n,&m);
init();
for(i=0;i<n-1;i++)
{
scanf("%s %s",ee,ss);
if(mp.find(ss)==mp.end())
{
s=cnt;mp[ss]=cnt++;
}
else s=mp[ss];
if(mp.find(ee)==mp.end())
{
e=cnt;mp[ee]=cnt++;
}
else e=mp[ee];
if(s!=e)
{
node[s].push_back(e);
in[e]++;
}
}
for(i=0;i<m;i++)
{
scanf("%s %s",ss,ee);
s=mp[ss];e=mp[ee];
out[i].s=s;out[i].e=e;
temp.nd=e;temp.id=i;
que[s].push_back(temp);
temp.nd=s;temp.id=i;
que[e].push_back(temp);
}
for(i=1;i<=n;i++) if(in[i]==0) break;//寻找根节点
LCA(i,0);
for(i=0;i<m;i++)
{
if(out[i].s==out[i].e)
printf("0\n");
else
if(out[i].s==ans[i])
printf("1\n");
else if(out[i].e==ans[i])
printf("%d\n",dis[out[i].s]-dis[ans[i]]);
else
printf("%d\n",dis[out[i].s]-dis[ans[i]]+1);
}
}
return 0;
}

代码转载自这里

例子:Connections between cities

Problem Description

After World War X, a lot of cities have been seriously damaged, and we need to rebuild those cities. However, some materials needed can only be produced in certain places. So we need to transport these materials from city to city. For most of roads had been totally destroyed during the war, there might be no path between two cities, no circle exists as well.
Now, your task comes. After giving you the condition of the roads, we want to know if there exists a path between any two cities. If the answer is yes, output the shortest path between them.

Input

Input consists of multiple problem instances.For each instance, first line contains three integers n, m and c, 2<=n<=10000, 0<=m<10000, 1<=c<=1000000. n represents the number of cities numbered from 1 to n. Following m lines, each line has three integers i, j and k, represent a road between city i and city j, with length k. Last c lines, two integers i, j each line, indicates a query of city i and city j.

Output

For each problem instance, one line for each query. If no path between two cities, output “Not connected”, otherwise output the length of the shortest path between them.

Sample Input

5 3 2
1 3 2
2 4 3
5 2 3
1 4
4 5

Sample Output

Not connected
6

Hint

Huge input, scanf recommended.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
#include<stdio.h>
#include<string.h>
#include<vector>
using namespace std;
#define Size 11111
struct Edge
{
int y,val;
}temp;
struct Query
{
int y,id;
}mid;
int pare[Size],ance[Size],vis[Size],dis[Size],rank[Size],ans[1000000+100],n,m,c,tree[Size];
vector<struct Query>que[Size];
vector<struct Edge>node[Size];
void init()
{

int i;
for(i=0;i<=n;i++)
{
vis[i]=0;
pare[i]=i;
dis[i]=0;
rank[i]=1;
que[i].clear();
node[i].clear();
}
memset(ans,-1,sizeof(ans));
}
int find(int x)
{

return pare[x]==x?x:pare[x]=find(pare[x]);
}
/*
void Union(int x,int y)
{
x=find(x);
y=find(y);
if(x!=y)
{
if(rank[x]>rank[y])
{
rank[x]+=rank[y];
pare[y]=x;
}
else
{
rank[y]+=rank[x];
pare[x]=y;
}
}
}
*/

void LCA(int root,int d,int k)//k表示是以第k个点作为根的树
{

int i,sz,nd1,nd2;
vis[root]=1; //已经遍历过的点 要标记一下 不要
tree[root]=k;dis[root]=d;
// ance[root]=root;
sz=node[root].size();
for(i=0;i<sz;i++)
{
nd2=node[root][i].y;
if(!vis[nd2])
{
LCA(nd2,d+node[root][i].val,k);
// Union(node[root][i].y,root);//用带rank的幷查集操作答案不对 不知道why
int w=find(nd2),m=find(root);
if(w!=m)
{
pare[w]=m;//这样才对
}
//ance[find(node[root][i].y)]=root;
}
}
sz=que[root].size();
for(i=0;i<sz;i++)
{
nd1=root;
nd2=que[root][i].y;
if(vis[nd2]&&tree[nd1]==tree[nd2])//如果 nd1 nd2 的跟是同一个点 则是同一棵树上的
{
ans[que[root][i].id]=dis[nd1]+dis[nd2]-2*dis[find(nd2)];
}
}
}
int main()
{

int i,j,x,y,val;
while(scanf("%d %d %d",&n,&m,&c)!=EOF)
{
init();
for(i=0;i<m;i++)
{
scanf("%d %d %d",&x,&y,&val);
if(x!=y)
{
temp.y=y;temp.val=val;
node[x].push_back(temp);
temp.y=x;
node[y].push_back(temp);//路是2个方向都可以通行的
}
}
for(i=0;i<c;i++)
{
scanf("%d %d",&x,&y);
mid.id=i;
mid.y=y;
que[x].push_back(mid);
mid.y=x;
que[y].push_back(mid);
}
for(i=1;i<=n;i++)
{
LCA(i,0,i);//以每一个节点作为根节点去深度搜索 找出每个点作为根的所有最近公共祖先
}
for(i=0;i<c;i++)
{
if(ans[i]==-1)
printf("Not connected\n");
else
printf("%d\n",ans[i]);
}
}
return 0;
}
//本题给的是一个森林 而不是一颗树,由于在加入边的时候,我们让2个方向都能走这样就形成了一个强连通的快,
//对于这个快来说,不管从快上那点出发 都可以遍历这个快上的所有的点,且相对距离是一样的

代码转载自这里

ST算法

ST(Sparse Table)算法是一个非常有名的在线处理RMQ问题的算法,它可以在O(nlogn)时间内进行预处理,然后在O(1)时间内回答每个查询。

ST(Sparse Table)算法是基于动态规划的,用f[i][j]表示区间起点为j长度为2^i的区间内的最小值所在下标,通俗的说,就是区间[j, j + 2^i)的区间内的最小值的下标。

从定义可知,这种表示法的区间长度一定是2的幂,所以除了单位区间(长度为1的区间)以外,任意一个区间都能够分成两份,并且同样可以用这种表示法进行表示,[j, j + 2^i)的区间可以分成[j, j+2^(i-1))和[j + 2^i),于是可以列出状态转移方程为: f[i][j] = RMQ( f[i-1][j], f[i-1][j+2^(i-1)] )。

f数组记录的是长度为偶数的子串的最值,对于正常的查询的话,并不可能每一次都是查询偶数长的区间,所以还需要在进一步的处理一下。对于查询区间[a, b], 可以将其划分为两个长度相等的偶数的子区间[a, a+2^k],[b-2^k, b],这两个区间有可能是相交的,但是不影响对区间最值的求解。现在只要根据a、b求出k,就可以知道[a, b]区间的最值了,为:min/max{f[k][a], f[k][b-(1<<k)+1]}.

对于K,需要满足a+2^k-1 >= b-2^k,则2^(k+1) >= (b-a+1), 两边取对数(以2为底),得 k+1 >= lg(b-a+1),则k >= lg(b-a+1) - 1,k只要需要取最小的满足条件的整数即可。

初始化:

1
2
3
4
5
6
7
8
9
10
//n为元数的个数
//bitn为n的二进制位数,取下整(int)(log(n)/log(2))
for (int i=0; i<n; ++i)
f[i][0]=input[i];
for (int j=1; j<bitn; ++j)
for (int i=0; i<n; ++i)
{
if (i+(1<<(j-1))>=n) break;
f[i][j]=max(f[i][j-1],f[i+(1<<(j-1))][j-1]);
}

查询:

1
2
3
4
5
int query(int s,int e)  //查询区间[s,e]的最值
{
int k=(int)((log(e-s+1.0)/log(2.0)));
return max(f[s][k],f[e-(1<<k)+1][k]);
}

线段树

是一种二叉搜索树,它将一个区间划分成一些单元区间,每个单元区间对应线段树中的一个叶结点。
对于线段树中的每一个非叶子节点[a,b],它的左子树表示的区间为[a,(a+b)/2],右子树表示的区间为[(a+b)/2+1,b]。因此线段树是平衡二叉树。叶节点数目为N,即整个线段区间的长度。
线段树主要的操作有建树、更新节点、查找三种操作

build

线段树使用数组来表示二叉树,其建树代码如下:

1
2
3
4
5
6
7
8
#define lchild rt << 1, l, m
#define rchild rt << 1 | 1, m + 1, r
void build(int rt = 1, int l = 1, int r = N) {
if (l == r) { std::cin >> tree[rt]; return; }
int m = (l + r) >> 1;
build(lchild); build(rchild);
push_up(rt);
}

节点数据向上更新

线段树的作用有很多种,可以表示区间的最大值或是最小值,也可以表示区间的和等,所以具体的更新操作update或是push_up操作需要根据功能来决定。下面是两种不用功能时的push_up操作:

1
2
3
4
5
6
7
8
9
/* 对于区间求和 */
void push_up(int rt) {
tree[rt] = tree[rt << 1] + tree[rt << 1 | 1];
}

/* 对于区间求最大值 */
void push_up(int rt) {
tree[rt] = max(tree[rt << 1], tree[rt << 1 | 1]);
}

节点懒惰标记下推

对于区间求和, 原子数组值需要加上lazy标记乘以子树所统计的区间长度。 len为父节点统计的区间长度, 则len - (len >> 1)为左子树区间长度, len >> 1为右子树区间长度。

1
2
3
4
5
6
7
void push_down(int rt, int len) {
tree[rt << 1] += lazy[rt] * (len - (len >> 1));
lazy[rt << 1] += lazy[rt];
tree[rt << 1 | 1] += lazy[rt] * (len >> 1);
lazy[rt << 1 | 1] += lazy[rt];
lazy[rt] = 0;
}

对于区间求最大值, 子树的值不需要乘以长度, 所以不需要传递参数len。

1
2
3
4
5
6
7
void push_down(int rt) {
tree[rt << 1] += lazy[rt];
lazy[rt << 1] += lazy[rt];
tree[rt << 1 | 1] += lazy[rt];
lazy[rt << 1 | 1] += lazy[rt];
lazy[rt] = 0;
}

update

update操作也是根据具体功能来决定,下面的代码为单点更新:

1
2
3
4
5
6
7
8
9
10
11
12
#define lchild rt << 1, l, m
#define rchild rt << 1 | 1, m + 1, r
void update(int p, int delta, int rt = 1, int l = 1, int r = N) {
if (l == r) {
tree[rt] += delta;
return;
}
int m = (l + r) >> 1;
if (p <= m) update(p, delta, lchild);
else update(p, delta, rchild);
push_up(rt);
}

成段更新,需要用到lazy来提高效率,下面代码是区间和的情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#define lchild rt << 1, l, m
#define rchild rt << 1 | 1, m + 1, r
void update(int L, int R, int delta, int rt = 1, int l = 1, int r = N) {
if (L <= l && r <= R) {
tree[rt] += delta * (r - l + 1);
lazy[rt] += delta;
return;
}
if (lazy[rt]) push_down(rt, r - l + 1);
int m = (l + r) >> 1;
if (L <= m) update(L, R, delta, lchild);
if (R > m) update(L, R, delta, rchild);
push_up(rt);
}

query

查询也是要根据具体功能来实现,下面是查询区间和的代码

1
2
3
4
5
6
7
8
9
10
#define lchild rt << 1, l, m
#define rchild rt << 1 | 1, m + 1, r
int query(int L, int R, int rt = 1, int l = 1, int r = N) {
if (L <= l && r <= R) return tree[rt];
if (lazy[rt]) push_down(rt, r - l + 1);
int m = (l + r) >> 1, ret = 0;
if (L <= m) ret += query(L, R, lchild);
if (R > m) ret += query(L, R, rchild);
return ret;
}

例子:hdu 1394 Minimum Inversion Number(线段树)

Problem Description

The inversion number of a given number sequence a1, a2, …, an is the number of pairs (ai, aj) that satisfy i < j and ai > aj.

For a given sequence of numbers a1, a2, …, an, if we move the first m >= 0 numbers to the end of the seqence, we will obtain another sequence. There are totally n such sequences as the following:

a1, a2, …, an-1, an (where m = 0 - the initial seqence)
a2, a3, …, an, a1 (where m = 1)
a3, a4, …, an, a1, a2 (where m = 2)

an, a1, a2, …, an-1 (where m = n-1)

You are asked to write a program to find the minimum inversion number out of the above sequences.

Input

The input consists of a number of test cases. Each case consists of two lines: the first line contains a positive integer n (n <= 5000); the next line contains a permutation of the n integers from 0 to n-1.

Output

For each case, output the minimum inversion number on a single line.

Sample Input

10
1 3 6 9 0 8 5 7 4 2

Sample Output

16

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
#include <cstdio>
#include <algorithm>
using namespace std;

#define lson l , m , rt << 1
#define rson m + 1 , r , rt << 1 | 1
const int maxn = 5555;
int sum[maxn<<2];
void PushUP(int rt) {
sum[rt] = sum[rt<<1] + sum[rt<<1|1];
}
void build(int l,int r,int rt) {
sum[rt] = 0;
if (l == r) return ;
int m = (l + r) >> 1;
build(lson);
build(rson);
}
void update(int p,int l,int r,int rt) {
if (l == r) {
sum[rt] ++;
return ;
}
int m = (l + r) >> 1;
if (p <= m) update(p , lson);
else update(p , rson);
PushUP(rt);
}
int query(int L,int R,int l,int r,int rt) {
if (L <= l && r <= R) {
return sum[rt];
}
int m = (l + r) >> 1;
int ret = 0;
if (L <= m) ret += query(L , R , lson);
if (R > m) ret += query(L , R , rson);
return ret;
}
int x[maxn];
int main() {
int n;
while (~scanf("%d",&n)) {
build(0 , n - 1 , 1);
int sum = 0;
for (int i = 0 ; i < n ; i ++) {
scanf("%d",&x[i]);
sum += query(x[i] , n - 1 , 0 , n - 1 , 1);
update(x[i] , 0 , n - 1 , 1);
}
int ret = sum;
for (int i = 0 ; i < n ; i ++) {
sum += n - x[i] - x[i] - 1;
ret = min(ret , sum);
}
printf("%d\n",ret);
}
return 0;
}

例子:Billboard

Problem Description

At the entrance to the university, there is a huge rectangular billboard of size h*w (h is its height and w is its width). The board is the place where all possible announcements are posted: nearest programming competitions, changes in the dining room menu, and other important information.

On September 1, the billboard was empty. One by one, the announcements started being put on the billboard.

Each announcement is a stripe of paper of unit height. More specifically, the i-th announcement is a rectangle of size 1 * wi.

When someone puts a new announcement on the billboard, she would always choose the topmost possible position for the announcement. Among all possible topmost positions she would always choose the leftmost one.

If there is no valid location for a new announcement, it is not put on the billboard (that’s why some programming contests have no participants from this university).

Given the sizes of the billboard and the announcements, your task is to find the numbers of rows in which the announcements are placed.

Input

There are multiple cases (no more than 40 cases).

The first line of the input file contains three integer numbers, h, w, and n (1 <= h,w <= 10^9; 1 <= n <= 200,000) - the dimensions of the billboard and the number of announcements.

Each of the next n lines contains an integer number wi (1 <= wi <= 10^9) - the width of i-th announcement.

Output

For each announcement (in the order they are given in the input file) output one number - the number of the row in which this announcement is placed. Rows are numbered from 1 to h, starting with the top row. If an announcement can’t be put on the billboard, output “-1” for this announcement.

Sample Input

3 5 5
2
4
3
3
3

Sample Output

1
2
1
3
-1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#include <cstdio>
#include <algorithm>
using namespace std;

#define lson l , m , rt << 1
#define rson m + 1 , r , rt << 1 | 1
const int maxn = 222222;
int h , w , n;
int MAX[maxn<<2];
void PushUP(int rt) {
MAX[rt] = max(MAX[rt<<1] , MAX[rt<<1|1]);
}
void build(int l,int r,int rt) {
MAX[rt] = w;
if (l == r) return ;
int m = (l + r) >> 1;
build(lson);
build(rson);
}
int query(int x,int l,int r,int rt) {
if (l == r) {
MAX[rt] -= x;
return l;
}
int m = (l + r) >> 1;
int ret = (MAX[rt<<1] >= x) ? query(x , lson) : query(x , rson);
PushUP(rt);
return ret;
}
int main() {
while (~scanf("%d%d%d",&h,&w,&n)) {
if (h > n) h = n;
build(1 , h , 1);
while (n --) {
int x;
scanf("%d",&x);
if (MAX[1] < x) puts("-1");
else printf("%d\n",query(x , 1 , h , 1));
}
}
return 0;
}

例子:Just a Hook

Problem Description

In the game of DotA, Pudge’s meat hook is actually the most horrible thing for most of the heroes. The hook is made up of several consecutive metallic sticks which are of the same length.

Now Pudge wants to do some operations on the hook.

Let us number the consecutive metallic sticks of the hook from 1 to N. For each operation, Pudge can change the consecutive metallic sticks, numbered from X to Y, into cupreous sticks, silver sticks or golden sticks.
The total value of the hook is calculated as the sum of values of N metallic sticks. More precisely, the value for each kind of stick is calculated as follows:

For each cupreous stick, the value is 1.
For each silver stick, the value is 2.
For each golden stick, the value is 3.

Pudge wants to know the total value of the hook after performing the operations.
You may consider the original hook is made up of cupreous sticks.

Input

The input consists of several test cases. The first line of the input is the number of the cases. There are no more than 10 cases.
For each case, the first line contains an integer N, 1<=N<=100,000, which is the number of the sticks of Pudge’s meat hook and the second line contains an integer Q, 0<=Q<=100,000, which is the number of the operations.
Next Q lines, each line contains three integers X, Y, 1<=X<=Y<=N, Z, 1<=Z<=3, which defines an operation: change the sticks numbered from X to Y into the metal kind Z, where Z=1 represents the cupreous kind, Z=2 represents the silver kind and Z=3 represents the golden kind.

Output

For each case, print a number in a line representing the total value of the hook after the operations. Use the format in the example.

Sample Input

1
10
2
1 5 2
5 9 3

Sample Output

Case 1: The total value of the hook is 24.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
#include <cstdio>
#include <algorithm>
using namespace std;

#define lson l , m , rt << 1
#define rson m + 1 , r , rt << 1 | 1
const int maxn = 111111;
int h , w , n;
int col[maxn<<2];
int sum[maxn<<2];
void PushUp(int rt) {
sum[rt] = sum[rt<<1] + sum[rt<<1|1];
}
void PushDown(int rt,int m) {
if (col[rt]) {
col[rt<<1] = col[rt<<1|1] = col[rt];
sum[rt<<1] = (m - (m >> 1)) * col[rt];
sum[rt<<1|1] = (m >> 1) * col[rt];
col[rt] = 0;
}
}
void build(int l,int r,int rt) {
col[rt] = 0;
sum[rt] = 1;
if (l == r) return ;
int m = (l + r) >> 1;
build(lson);
build(rson);
PushUp(rt);
}
void update(int L,int R,int c,int l,int r,int rt) {
if (L <= l && r <= R) {
col[rt] = c;
sum[rt] = c * (r - l + 1);
return ;
}
PushDown(rt , r - l + 1);
int m = (l + r) >> 1;
if (L <= m) update(L , R , c , lson);
if (R > m) update(L , R , c , rson);
PushUp(rt);
}
int main() {
int T , n , m;
scanf("%d",&T);
for (int cas = 1 ; cas <= T ; cas ++) {
scanf("%d%d",&n,&m);
build(1 , n , 1);
while (m --) {
int a , b , c;
scanf("%d%d%d",&a,&b,&c);
update(a , b , c , 1 , n , 1);
}
printf("Case %d: The total value of the hook is %d.\n",cas , sum[1]);
}
return 0;
}

例子:A Simple Problem with Integers

Description

You have N integers, A1, A2, … , AN. You need to deal with two kinds of operations. One type of operation is to add some given number to each number in a given interval. The other is to ask for the sum of numbers in a given interval.

Input

The first line contains two numbers N and Q. 1 ≤ N,Q ≤ 100000.
The second line contains N numbers, the initial values of A1, A2, … , AN. -1000000000 ≤ Ai ≤ 1000000000.
Each of the next Q lines represents an operation.
“C a b c” means adding c to each of Aa, Aa+1, … , Ab. -10000 ≤ c ≤ 10000.
“Q a b” means querying the sum of Aa, Aa+1, … , Ab.

Output

You need to answer all Q commands in order. One answer in a line.

Sample Input

10 5
1 2 3 4 5 6 7 8 9 10
Q 4 4
Q 1 10
Q 2 4
C 3 6 3
Q 2 4

Sample Output

4
55
9
15

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
#include <cstdio>
#include <algorithm>
using namespace std;

#define lson l , m , rt << 1
#define rson m + 1 , r , rt << 1 | 1
#define LL long long
const int maxn = 111111;
LL add[maxn<<2];
LL sum[maxn<<2];
void PushUp(int rt) {
sum[rt] = sum[rt<<1] + sum[rt<<1|1];
}
void PushDown(int rt,int m) {
if (add[rt]) {
add[rt<<1] += add[rt];
add[rt<<1|1] += add[rt];
sum[rt<<1] += add[rt] * (m - (m >> 1));
sum[rt<<1|1] += add[rt] * (m >> 1);
add[rt] = 0;
}
}
void build(int l,int r,int rt) {
add[rt] = 0;
if (l == r) {
scanf("%lld",&sum[rt]);
return ;
}
int m = (l + r) >> 1;
build(lson);
build(rson);
PushUp(rt);
}
void update(int L,int R,int c,int l,int r,int rt) {
if (L <= l && r <= R) {
add[rt] += c;
sum[rt] += (LL)c * (r - l + 1);
return ;
}
PushDown(rt , r - l + 1);
int m = (l + r) >> 1;
if (L <= m) update(L , R , c , lson);
if (m < R) update(L , R , c , rson);
PushUp(rt);
}
LL query(int L,int R,int l,int r,int rt) {
if (L <= l && r <= R) {
return sum[rt];
}
PushDown(rt , r - l + 1);
int m = (l + r) >> 1;
LL ret = 0;
if (L <= m) ret += query(L , R , lson);
if (m < R) ret += query(L , R , rson);
return ret;
}
int main() {
int N , Q;
scanf("%d%d",&N,&Q);
build(1 , N , 1);
while (Q --) {
char op[2];
int a , b , c;
scanf("%s",op);
if (op[0] == 'Q') {
scanf("%d%d",&a,&b);
printf("%lld\n",query(a , b , 1 , N , 1));
} else {
scanf("%d%d%d",&a,&b,&c);
update(a , b , c , 1 , N , 1);
}
}
return 0;
}

并查集

并查集主要是以数组的形式来记录一个森林结构,数组中存储每一个元素对应的父元素。在森林结构中,一棵树代表一个集合,树的根节点为该集合的代表。并查集主要有三中操作:MakeSet、find和Union。

MakeSet

该操作组要初始化并查集,具体代码为:

1
2
3
4
5
6
#define NODESIZE 1000
int father[NODESIZE];
void makeSet(int size){ //初始的集合个数
for(int i=0; i<size; i++)
father[i] = i;
}

find

该操作用来查找一个元素所在集合的代表元素。具体操作为:

1
2
3
4
5
int find(int x){
while(father[x] != x)
x = father[x];
return x;
}

由于并查集只需要得到每一个元素所在的集合的代表,所以在查找的时候可以采用路劲压缩的方法,对每一个元素的father直接指向对应的代表节点,以后在查找代表节点的时候,就可以在常数时间内得到。
并查集之Union

1
2
3
4
5
6
7
8
9
10
11
12
//路径压缩的查找
int find(int x){
int p = x;
while(father[x] != x)
x = father[x];
while(father[p] != x){
int temp = father[p];
father[p] = x;
p = temp;
}
return x;
}

Union

该操作主要用来合并两个集合,为了使得合并后的树高度尽量小,从而保证查找的效率,我们可以使用一个数组来记录每一棵树的高度,在合并的时候,将高度小的树合并到高度大的树。
并查集之Union
具体代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
void union(int x, int y){
//x和y处于同一个集合中,直接忽略
if((x=find(x)) == (y=find(y))) return; //这里的对x和y的赋值,方便下面的操作,此时的x和y为其所在集合的代表
if(rank[x] < rank[y]){
father[x] = y;
}
else{
father[y] = x;
if(rank[y] == rank[x])
rank[x]++;
}

}

UDP之connect

在UDP中,也是可以调用connect系统调用的。但是其作用并不是TCP中的与服务器发起三次握手,他只是在系统中注册对端的地址,以供后面使用。
因为在udp编程中,如果需要发送数据给对端,那么需要使用sendTo函数,在参数中指明对端的地址,此时系统所做的事情是,先调用connect函数(注册对端),接着再发送数据,然后再调用connect函数(取消注册)。如果先调用connect函数的话,那么在以后的发送数据时,就不需要使用sendTo函数了(也不能使用),直接使用send和receive函数就可以进行数据的发送和接受了。因为调用connect注册后,此时系统默认该UDP只能与注册的对端发生数据交互,不能与其他的对端进行通信,所以在发送数据的时候就不需要使用sendto了。而至于receive,在调用connect后,如果不是所注册的对端发来的数据,系统会默认将其丢掉,只接受所注册的对端发来的数据。UDP编程使用connect系统调用,具体有以下几个特点:

  1. TCP中调用connect会引起三次握手,client与server建立连结.UDP中调用connect内核仅仅把对端ip&port记录下来.

  2. UDP中可以多次调用connect,TCP只能调用一次connect.(UDP多次调用connect有两种用途:1,指定一个新的ip&port连结. 2,断开和之前的ip&port的连结)

  3. 对于连续向相同的对端发送数据时,调用connect可以提高效率,因为每一次的sendto都需要经过三个阶段:建立连接-》发送数据-》断开连接
  4. 采用connect的UDP发送接受报文可以调用send,write和recv,read操作.当然也可以调用sendto,recvfrom. 调用sendto的时候第五个参数必须是NULL,第六个参数是0.调用recvfrom,recv,read系统调用只能获取到先前connect的ip&port发送的报文.
  5. 由已连接的UDP套接口引发的异步错误,返回给他们所在的进程。相反未连接UDP套接口不接收任何异步错误给一个UDP套接口。如果对端没启动,默认情况下发送的包对应的ICMP回射包不会给调用进程,调用了connect之后就可以收到该错误。

第6点主要是因为在UDP规则中,如果收到UDP数据报而且目的端口与某个正在使用的进程不相符,那么UDP返回一个ICMP不可达报文。所有的ICMP差错报告报文中的数据字段都具有同样的格式。将收到的需要进行差错报告IP数据报的首部和数据字段的前8个字节提取出来,作为ICMP报文的数据字段。再加上响应的ICMP差错报告报文的前8个字节,就构成了ICMP差错报告报文。提取收到的数据报的数据字段的前8个字节是为了得到运输层的端口号(对于TCP和UDP)以及运输层报文的发送序号(对于TCP)。

从发送到收到icmp是有一定的时延的, 如果是Sendto往二个目的地址写数据报,此时1成功1失败,这种情况下如果这时候内核收到icmp报文,就不知道是哪个sendto。虽然icmp可以获得传输层的端口,但是在sendTo的目的ip和端口一样的情况下,也不能辨别出是哪一个sendto造成的。

分布式锁

基于redis的分布式锁:redis加锁需要用到命令setnt(如果key不存在,那么设置对应的值),对于锁,需要考虑两个问题:网络问题或宕机导致解锁失败和加锁不成功时如何等待(sleep或是一直访问肯定不行)。对于第一个问题,可以采用redis的expire命令,来设置key的过期时间。对于第二个问题,可以利用redis的发布与订阅来实现阻塞等待。一个锁对应一个channel,如果加锁失败,那么订阅该chennel,阻塞监听该chennel知道收到消息为止,此时说明解锁了,可以进一步尝试加锁。在解锁时,需要发布一条消息,来唤醒所有等待在channel上的客户端。

对于可重入锁,也就是一个获得锁的客户端,可以再一次的加锁,此时可以在setnt的时候,设置key的value为客户端的id+进程id+加锁次数。如果在setnt失败后,那么先获得key对应的value,判断锁的持有者是否为本进程,是的话,可以直接将key的value对应的次数加1,利用set进行设置,再一次加锁。在解锁的时候,如果锁的次数大于1,那么直接减去1就好,如果锁的次数为1的话,那么此时需要真正的解锁,直接把del key,并发布一条消息,通知等待的客户端。可以参考redissonlock来学习具体的实现机制:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155

//加锁的实现函数,成功返回null,不成功返回锁的存活时间,-1表示锁永不超时
private Long tryLockInner() {
//保存锁的状态: 客户端UUID+线程ID来唯一标识某一JVM实例的某一线程
final LockValue currentLock = new LockValue(id, Thread.currentThread().getId());

currentLock.incCounter(); //用来保存重入次数, 实现可重入功能, 初始情况是1



//Redisson封装了交互的细节, 具体的逻辑为execute方法逻辑.

return connectionManager.write(getName(), new SyncOperation<LockValue, Long>() {

@Override

public Long execute(RedisConnection<Object, LockValue> connection) {

//如果key:haogrgr不存在, 就set并返回true, 否则返回false
Boolean res = connection.setnx(getName(), currentLock);
//如果设置失败, 那么表示有锁竞争了, 于是获取当前锁的状态, 如果拥有者是当前线程, 就累加重入次数并set新值
if (!res) {
//通过watch命令配合multi来实现简单的事务功能,这里需要监看key,有可能key因为超时被删除了。
connection.watch(getName());

LockValue lock = (LockValue) connection.get(getName());
//LockValue的equals实现为比较客户id和threadid是否一样
if (lock != null && lock.equals(currentLock)) {

lock.incCounter(); //如果当前线程已经获取过锁, 则累加加锁次数, 并set更新

connection.multi();

connection.set(getName(), lock);

if (connection.exec().size() == 1) {

return null; //set成功,

}

}

connection.unwatch();



//走到这里, 说明上面set的时候, 其他客户端在 watch之后->set之前 有其他客户端修改了key值

//则获取key的过期时间, 如果是永不过期, 则返回-1, 具体处理后面说明

Long ttl = connection.pttl(getName());

return ttl;

}

return null;

}

});

}

//加锁操作
public void lockInterruptibly(long leaseTime, TimeUnit unit) throws InterruptedException {

Long ttl;

if (leaseTime != -1) {

ttl = tryLockInner(leaseTime, unit);

} else {

ttl = tryLockInner(); //lock()方法调用会走的逻辑

}

// lock acquired
//加锁成功(新获取锁, 重入情况) tryLockInner会返回null, 失败会返回key超时时间, 或者-1(key未设置超时时间)
if (ttl == null) {

return; //加锁成功, 返回

}



//subscribe这个方法代码有点多, Redisson通过netty来和redis通讯, 然后subscribe返回的是一个Future类型,

//Future的awaitUninterruptibly()调用会阻塞, 然后Redisson通过Redis的pubsub来监听unlock的topic(getChannelName())

//例如, 5中所看到的命令 "PUBLISH" "redisson__lock__channel__{haogrgr}" "0"

//当解锁时, 会向名为 getChannelName() 的topic来发送解锁消息("0")

//而这里 subscribe() 中监听这个topic, 在订阅成功时就会唤醒阻塞在awaitUninterruptibly()的方法.

//所以线程在这里只会阻塞很短的时间(订阅成功即唤醒, 并不代表已经解锁)

subscribe().awaitUninterruptibly();



try {

while (true) { //循环, 不断重试lock

if (leaseTime != -1) {

ttl = tryLockInner(leaseTime, unit);

} else {

ttl = tryLockInner(); //不多说了

}

// lock acquired

if (ttl == null) {

break;

}


// 这里才是真正的等待解锁消息, 收到解锁消息, 就唤醒, 然后尝试获取锁, 成功返回, 失败则阻塞在acquire().

// 收到订阅成功消息, 则唤醒阻塞上面的subscribe().awaitUninterruptibly();

// 收到解锁消息, 则唤醒阻塞在下面的entry.getLatch().acquire();

RedissonLockEntry entry = ENTRIES.get(getEntryName());

if (ttl >= 0) {
//等待订阅的消息,ttl时间后超时,表示锁被自动删除
entry.getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);

} else {
//锁没有设置超时情况,需要一直等到有消息为止。
entry.getLatch().acquire();

}

}

} finally {

unsubscribe(); //加锁成功或异常,解除订阅

}

}

下面是解锁的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
public void unlock() {

connectionManager.write(getName(), new SyncOperation<Object, Void>() {

@Override

public Void execute(RedisConnection<Object, Object> connection) {


LockValue lock = (LockValue) connection.get(getName());

if (lock != null) {

LockValue currentLock = new LockValue(id, Thread.currentThread().getId());

if (lock.equals(currentLock)) {

if (lock.getCounter() > 1) {

lock.decCounter();

connection.set(getName(), lock);

} else {

unlock(connection);

}

} else {

throw new IllegalMonitorStateException("Attempt to unlock lock, not locked by current id: "

+ id + " thread-id: " + Thread.currentThread().getId());

}

} else {

// could be deleted

}

return null;

}

});

}



private void unlock(RedisConnection<Object, Object> connection) {

int counter = 0;
//尝试5次解锁,不成功则抛出异常
while (counter < 5) {

connection.multi();

connection.del(getName());

connection.publish(getChannelName(), unlockMessage);

List<Object> res = connection.exec();

if (res.size() == 2) {

return;

}

counter++;

}

throw new IllegalStateException("Can't unlock lock after 5 attempts. Current id: "

+ id + " thread-id: " + Thread.currentThread().getId());

}

JVM调优

JVM的调优主要在垃圾回收上,于是调优的主要工作主要集中在两个部分:堆空间的设置和垃圾回收器的选择

堆大小设置

JVM中最大堆大小有三方面限制:相关操作系统的数据模型(32-bt还是64-bit)限制;系统的可用虚拟内存限 制;系统的可用物理内存限制。32位系统下,一般限制在1.5G~2G;64为操作系统对内存无限制。在 Windows Server 2003 系统,3.5G物理内存,JDK5.0下测试,最大可设置为1478m。
典型设置:

1
2
3
4
java -Xmx3550m -Xms3550m -Xmn2g –Xss128k
-Xmx3550m:设置JVM最大可用内存为3550M。
-Xms3550m:设置JVM促使内存为3550m。此值可以设置与-Xmx相同,以避免每次垃圾回收完成 后JVM重新分配内存。
-Xmn2g:设置年轻代大小为2G。整个堆大小=年轻代大小 + 年老代大小 + 持久代大小。

持久代一般 固定大小为64m,所以增大年轻代后,将会减小年老代大小。此值对系统性能影响较大,Sun官方推荐配置为整个堆的3/8。
-Xss128k:设置每个线程的堆栈大小。JDK5.0以后每个线程堆栈大小为1M,以前每个线程堆栈大小 为256K。更具应用的线程所需内存大小进行调整。在相同物理内存下,减小这个值能生成更多的线 程。但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右。

1
2
java -Xmx3550m -Xms3550m -Xss128k -XX:NewRatio=4 -XX:SurvivorRatio=4
-XX:MaxPermSize=16m -XX:MaxTenuringThreshold=0

-XX:NewRatio=4:设置年轻代(包括Eden和两个Survivor区)与年老代的比值(除去持久代)。设 置为4,则年轻代与年老代所占比值为1:4,年轻代占整个堆栈的1/5

-XX:SurvivorRatio=4:设置年轻代中Eden区与Survivor区的大小比值。设置为4,则两个Survivor 区与一个Eden区的比值为2:4,一个Survivor区占整个年轻代的1/6

-XX:MaxPermSize=16m:设置持久代大小为16m。

-XX:MaxTenuringThreshold=0:设置垃圾最大年龄。如果设置为0的话,则年轻代对象不经过 Survivor区,直接进入年老代。对于年老代比较多的应用,可以提高效率。如果将此值设置为一个较大 值,则年轻代对象会在Survivor区进行多次复制,这样可以增加对象再年轻代的存活时间,增加在年轻代即被回收的概论。

回收器选择

JVM给了三种选择:串行收集器、并行收集器、并发收集器,但是串行收集器只适用于小数据量的情况,所以 这里的选择主要针对并行收集器和并发收集器。默认情况下,JDK5.0以前都是使用串行收集器,如果想使用其 他收集器需要在启动时加入相应参数。JDK5.0以后,JVM会根据当前系统配置进行判断。

吞吐量优先的并行收集器

如上文所述,并行收集器主要以到达一定的吞吐量为目标,适用于科学技术和后台处理等。

典型配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java -Xmx3800m -Xms3800m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:ParallelGCThreads=20
-XX:+UseParallelGC:选择垃圾收集器为并行收集器。此配置仅对年轻代有效。即上述配置下,
年轻代使用并发收集,而年老代仍旧使用串行收集。

-XX:ParallelGCThreads=20:配置并行收集器的线程数,即:同时多少个线程一起进行垃圾回收。
此值最好配置与处理器数目相等。


java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:ParallelGCThreads=20
-XX:+UseParallelOldGC
-XX:+UseParallelOldGC:配置年老代垃圾收集方式为并行收集。JDK6.0支持对年老代并行收集。

java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:MaxGCPauseMillis=100
-XX:MaxGCPauseMillis=100:设置每次年轻代垃圾回收的最长时间,如果无法满足此时间,
JVM会自动调整年轻代大小,以满足此值


java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseParallelGC XX:MaxGCPauseMillis=100
-XX:+UseAdaptiveSizePolicy
-XX:+UseAdaptiveSizePolicy:设置此选项后,并行收集器会自动选择年轻代区大小和相应的Survivor区比例,
以达到目标系统规定的最低相应时间或者收集频率等,此值建议使用并行收集器时,

一直打开。

响应时间优先的并发收集器

如上文所述,并发收集器主要是保证系统的响应时间,减少垃圾收集时的停顿时间。适用于应用服务器、电信 领域等。

典型配置:

1
2
3
4
5
6
7
8
9
10
11
12
java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:ParallelGCThreads=20
-XX:+UseConcMarkSweepGC -XX:+UseParNewGC
-XX:+UseConcMarkSweepGC:设置年老代为并发收集。测试中配置这个以后,-XX:NewRatio=4
的配置失效了,原因不明。所以,此时年轻代大小最好用-Xmn设置。

-XX:+UseParNewGC: 设置年轻代为并行收集。可与CMS收集同时使用。JDK5.0以上,
JVM会根据系统配置自行设置,所以无需再设置此值。


java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseConcMarkSweepGC -XX:CMSFullGCsBeforeCompaction=5
-XX:+UseCMSCompactAtFullCollection
-XX:CMSFullGCsBeforeCompaction:由于并发收集器不对内存空间进行压缩、整理,所以运行一段时间以后会产生“碎片”,
使得运行效率降低。此值设置运行多少次GC以后对内存空间进行压缩、整理。

-XX:+UseCMSCompactAtFullCollection:打开对年老代的压缩。可能会影响性能,但是可以消除 碎片

常见配置汇总

堆设置

1
2
3
4
5
6
7
-Xms:初始堆大小
-Xmx:最大堆大小
-XX:NewSize=n:设置年轻代大小
-XX:NewRatio=n:设置年轻代和年老代的比值。如:3,表示年轻代与年老代比值为13,年轻代占整个年 轻代年老代和的1/4
-XX:SurvivorRatio=n:年轻代中Eden区与两个Survivor区的比值。注意Survivor区有两个。如:3,表示
Eden:Survivor=3:2,一个Survivor区占整个年轻代的1/5

-XX:MaxPermSize=n:设置持久代大小

收集器设置

1
2
3
4
-XX:+UseSerialGC:设置串行收集器
-XX:+UseParallelGC:设置并行收集器
-XX:+UseParalledlOldGC:设置并行年老代收集器
-XX:+UseConcMarkSweepGC:设置并发收集器

垃圾回收统计信息

1
2
3
4
-XX:+PrintGC
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps
-Xloggc:filename

并行收集器设置

1
2
3
-XX:ParallelGCThreads=n:设置并行收集器收集时使用的CPU数。并行收集线程数。
-XX:MaxGCPauseMillis=n:设置并行收集最大暂停时间
-XX:GCTimeRatio=n:设置垃圾回收时间占程序运行时间的百分比。公式为1/(1+n)

并发收集器设置

1
2
-XX:+CMSIncrementalMode:设置为增量模式。适用于单CPU情况。
-XX:ParallelGCThreads=n:设置并发收集器年轻代收集方式为并行收集时,使用的CPU数。并行收集线程数。

异常分析

持久代被占满

异常:java.lang.OutOfMemoryError: PermGen space
说明:

Perm空间被占满。无法为新的class分配存储空间而引发的异常。这个异常以前是没有的,但是在Java反射 大量使用的今天这个异常比较常见了。主要原因就是大量动态反射生成的类不断被加载,最终导致Perm区被占 满。
更可怕的是,不同的classLoader即便使用了相同的类,但是都会对其进行加载,相当于同一个东西,如果有 N个classLoader那么他将会被加载N次。因此,某些情况下,这个问题基本视为无解。当然,存在大量 classLoader和大量反射类的情况其实也不多。

解决:
  1. -XX:MaxPermSize=16m
  2. 换用JDK。比如JRocket。

堆栈溢出

异常:java.lang.StackOverflowError
说明:

这个就不多说了,一般就是递归没返回,或者循环调用造成线程堆栈满

异常:Fatal: Stack size too small

说明:

java中一个线程的空间大小是有限制的。JDK5.0以后这个值是1M。与这个线程相关的数据将会保存在其 中。但是当线程空间满了以后,将会出现上面异常。

解决:

增加线程栈大小。-Xss2m。但这个配置无法解决根本问题,还要看代码部分是否有造成泄漏的部分。

系统内存被占满

异常:java.lang.OutOfMemoryError: unable to create new native thread
说明:

这个异常是由于操作系统没有足够的资源来产生这个线程造成的。系统创建线程时,除了要在Java堆中分配 内存外,操作系统本身也需要分配资源来创建线程。因此,当线程数量大到一定程度以后,堆中或许还有空 间,但是操作系统分配不出资源来了,就出现这个异常了。
分配给Java虚拟机的内存愈多,系统剩余的资源就越少,因此,当系统内存固定时,分配给Java虚拟机的内存 越多,那么,系统总共能够产生的线程也就越少,两者成反比的关系。同时,可以通过修改-Xss来减少分配给 单个线程的空间,也可以增加系统总共内生产的线程数。

解决:
  1. 重新设计系统减少线程数量。
  2. 线程数量不能减少的情况下,通过-Xss减小单个线程大小。以便能生产更多的线程。

以上的内容转载自JavaEye上的文章,感谢作者的总结。下面的链接是该作者在JVM调优上的一系列文章,个人觉得写的很好,于是在这里也分享一下。
JVM调优总结.pdf

垃圾回收

java垃圾回收机制主要有两个步骤:标记和回收

标记

标记过程主要标记那些当前还在使用的对象,主要有两种方法:引用计数器和跟踪遍历

引用计数器

引用计数是垃圾收集器中的早期策略。在这种方法中,堆中每个对象(不是引用)都有一个引用计数。当一个对象被创建时,且将该对象分配给一个变量,该变量计数设置为1。当任何其它变量被赋值为这个对象的引用时,计数加1(a = b,则b引用的对象+1),但当一个对象的某个引用超过了生命周期或者被设置为一个新值时,对象的引用计数减1。任何引用计数为0的对象可以被当作垃圾收集。当一个对象被垃圾收集时,它引用的任何对象计数减1。

优点:引用计数收集器可以很快的执行,交织在程序运行中。对程序不被长时间打断的实时环境比较有利。

缺点: 无法检测出循环引用。如父对象有一个对子对象的引用,子对象反过来引用父对象。这样,他们的引用计数永远不可能为0.

跟踪遍历

现在大多数JVM采用对象引用遍历。对象引用遍历从一组对象开始,沿着整个对象图上的每条链接,递归确定可到达(reachable)的对象。如果某对象不能从这些根对象的一个(至少一个)到达,则将它作为垃圾收集。在对象遍历阶段,GC必须记住哪些对象可以到达,以便删除不可到达的对象,这称为标记(marking)对象。

回收

采用引用计数器的GC一般可以混合在程序运行过程中,对没有被引用的对象及时清除,不需要打断程序的运行,但是除了无法删除循环引用的对象之外,这种垃圾回收会使得内存出现碎片。这种机制回收与标记一起执行,两个阶段区分不大。但对于跟踪遍历,回收阶段就有不同的回收算法。

标记-清除收集器

这种收集器首先遍历对象图并标记可到达的对象,然后扫描堆栈以寻找未标记对象并释放它们的内存。这种收集器一般使用单线程工作并停止其他操作。并且,由于它只是清除了那些未标记的对象,而并没有对标记对象进行压缩,导致会产生大量内存碎片,从而浪费内存。

标记-压缩收集器

有时也叫标记-清除-压缩收集器,与标记-清除收集器有相同的标记阶段。在第二阶段,则把标记对象复制到堆栈的新域中以便压缩堆栈。这种收集器也停止其他操作。

复制收集器

这种收集器将堆栈分为两个域,常称为半空间。每次仅使用一半的空间,JVM生成的新对象则放在另一半空间中。GC运行时,它把可到达对象复制到另一半空间,从而压缩了堆栈。这种方法适用于短生存期的对象,持续复制长生存期的对象则导致效率降低。并且对于指定大小堆来说,需要两倍大小的内存,因为任何时候都只使用其中的一半。

增量收集器

增量收集器把堆栈分为多个域,每次仅从一个域收集垃圾,也可理解为把堆栈分成一小块一小块,每次仅对某一个块进行垃圾收集。这会造成较小的应用程序中断时间,使得用户一般不能觉察到垃圾收集器正在工作。

分代收集器

复制收集器的缺点是:每次收集时,所有的标记对象都要被拷贝,从而导致一些生命周期很长的对象被来回拷贝多次,消耗大量的时间。而分代收集器则可解决这个问题,分代收集器把堆栈分为两个或多个域,用以存放不同寿命的对象。JVM生成的新对象一般放在其中的某个域中。过一段时间,继续存在的对象(非短命对象)将获得使用期并转入更长寿命的域中。分代收集器对不同的域使用不同的算法以优化性能。

分代的垃圾回收策略,是基于这样一个事实:不同的对象的生命周期是不一样的。因此,不同生命周期的对象可以采取不同的回收算法,以便提高回收效率。

年轻代(Young Generation)

1、所有新生成的对象首先都是放在年轻代的。年轻代的目标就是尽可能快速的收集掉那些生命周期短的对象。

2、新生代内存按照8:1:1的比例分为一个eden区和两个survivor(survivor0,survivor1)区。一个Eden区,两个 Survivor区(一般而言)。大部分对象在Eden区中生成。回收时先将eden区存活对象复制到一个survivor0区,然后清空eden区,当这个survivor0区也存放满了时,则将eden区和survivor0区存活对象复制到另一个survivor1区,然后清空eden和这个survivor0区,此时survivor0区是空的,然后将survivor0区和survivor1区交换,即保持survivor1区为空, 如此往复。

3、当survivor1区不足以存放 eden和survivor0的存活对象时,就将存活对象直接存放到老年代。若是老年代也满了就会触发一次Full GC,也就是新生代、老年代都进行回收

4、新生代发生的GC也叫做Minor GC,MinorGC发生频率比较高(不一定等Eden区满了才触发)

年老代(Old Generation)

1、在年轻代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。

2、内存比新生代也大很多(大概比例是1:2),当老年代内存满时触发Major GC即Full GC,Full GC发生频率比较低,老年代对象存活时间比较长,存活率标记高。

持久代(Permanent Generation)

用于存放静态文件,如Java类、方法等。持久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些class,例如Hibernate 等,在这种时候需要设置一个比较大的持久代空间来存放这些运行过程中新增的类。

由于对象进行了分代处理,因此垃圾回收区域、时间也不一样。GC有两种类型:Scavenge GC和Full GC。

GC的执行机制

由于对象进行了分代处理,因此垃圾回收区域、时间也不一样。GC有两种类型:Scavenge GC和Full GC。

Scavenge GC

一般情况下,当新对象生成,并且在Eden申请空间失败时,就会触发Scavenge GC,对Eden区域进行GC,清除非存活对象,并且把尚且存活的对象移动到Survivor区。然后整理Survivor的两个区。这种方式的GC是对年轻代的Eden区进行,不会影响到年老代。因为大部分对象都是从Eden区开始的,同时Eden区不会分配的很大,所以Eden区的GC会频繁进行。因而,一般在这里需要使用速度快、效率高的算法,使Eden去能尽快空闲出来。

Full GC

对整个堆进行整理,包括Young、Tenured和Perm。Full GC因为需要对整个堆进行回收,所以比Scavenge GC要慢,因此应该尽可能减少Full GC的次数。在对JVM调优的过程中,很大一部分工作就是对于FullGC的调节。有如下原因可能导致Full GC:

1、年老代(Tenured)被写满

2、持久代(Perm)被写满

3、System.gc()被显示调用

4、上一次GC之后Heap的各域分配策略动态变化

内存管理

Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干不同的数据区域,这些区域都有各自的用途以及创建和销毁的时间。Java虚拟机所管理的内存将会包括以下几个运行时数据区域,如下图所示:

程序计数器

程序计数器,可以看做是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里,字节码解释器工作就是通过改变程序计数器的值来选择下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都要依赖这个计数器来完成。

多线程中,为了让线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间互不影响、独立存储,因此这块内存是 线程私有 的。

当线程正在执行的是一个Java方法,这个计数器记录的是在正在执行的虚拟机字节码指令的地址;当执行的是Native方法,这个计数器值为空。

此内存区域是唯一一个没有规定任何OutOfMemoryError情况的区域 。

Java虚拟机栈

Java虚拟机栈也是线程私有的 ,它的生命周期与线程相同。虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧用于存储局部变量表、操作数栈、动态链表、方法出口信息等。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。

局部变量表中存放了编译器可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用和returnAddress类型(指向了一条字节码指令的地址)。

如果扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常。

本地方法栈

本地方法栈与虚拟机的作用相似,不同之处在于虚拟机栈为虚拟机执行的Java方法服务,而本地方法栈则为虚拟机使用到的Native方法服务。有的虚拟机直接把本地方法栈和虚拟机栈合二为一。

会抛出stackOverflowError和OutOfMemoryError异常。

Java堆

Java堆是所有线程共享的一块内存区域,在虚拟机启动时创建,此内存区域的唯一目的就是存放对象实例 。
Java堆是垃圾收集器管理的主要区域。由于现在收集器基本采用分代回收算法,所以Java堆还可细分为:新生代和老年代。从内存分配的角度来看,线程共享的Java堆中可能划分出多个线程私有的分配缓冲区(TLAB)。

Java堆可以处于物理上不连续的内存空间,只要逻辑上连续的即可。在实现上,既可以实现固定大小的,也可以是扩展的。

如果堆中没有内存完成实例分配,并且堆也无法完成扩展时,将会抛出OutOfMemoryError异常。

方法区

方法区是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据 。

相对而言,垃圾收集行为在这个区域比较少出现,但并非数据进了方法区就永久的存在了,这个区域的内存回收目标主要是针对常量池的回收和对类型的卸载,

当方法区无法满足内存分配需要时,将抛出OutOfMemoryError异常。

运行时常量池:

是方法区的一部分,它用于存放编译期生成的各种字面量和符号引用。
以上内容来自这里

垃圾回收

Sun的JVM Generational Collecting(垃圾回收)原理是这样的:把对象分为年青代(Young)、年老代(Tenured)、持久代(Perm),对不同生命周期的对象使用不同的算法。(基于对对象生命周期分析)

Yong(新生代)

对象的内存分配,往大方向上讲就是在堆上分配,对象主要分配在新生代的Eden Space和From Space,少数情况下会直接分配在老年代。如果新生代的Eden Space和From Space的空间不足,则会发起一次GC,如果进行了GC之后,Eden Space和From Space能够容纳该对象就放在Eden Space和From Space。在GC的过程中,会将Eden Space和From Space中的存活对象移动到To Space,然后将Eden Space和From Space进行清理。如果在清理的过程中,To Space无法足够来存储某个对象,就会将该对象移动到老年代中。在进行了GC之后,使用的便是Eden space和To Space了,下次GC时会将存活对象复制到From Space,如此反复循环。当对象在Survivor区躲过一次GC的话,其对象年龄便会加1,默认情况下,如果对象年龄达到15岁,就会移动到老年代中。

一般来说,大对象会被直接分配到老年代,所谓的大对象是指需要大量连续存储空间的对象,最常见的一种大对象就是大数组,比如:

1
byte[] data = new byte[4*1024*1024]

这种一般会直接在老年代分配存储空间。

当然分配的规则并不是百分之百固定的,这要取决于当前使用的是哪种垃圾收集器组合和JVM的相关参数。

Tenured(年老代)

年老代存放从年轻代存活的对象。一般来说年老代存放的都是生命期较长的对象。

Perm(持久代)

用于存放静态文件,如今Java类、方法等。持久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些class,例如hibernate等,在这种时候需要设置一个比较大的持久代空间来存放这些运行过程中新增的类。持久代大小通过-XX:MaxPermSize=进行设置。持久代也就是下面所说的非堆内存,也是上面所说的方法区。

简单的概念:

堆(Heap)和非堆(Non-heap)内存

按照官方的说法:“Java 虚拟机具有一个堆,堆是运行时数据区域,所有类实例和数组的内存均从此处分配。堆是在 Java 虚拟机启动时创建的。”“在JVM中堆之外的内存称为非堆内存(Non-heap memory)”。可以看出JVM主要管理两种类型的内存:堆和非堆。简单来说堆就是Java代码可及的内存,是留给开发人员使用的;非堆就是JVM留给 自己用的,所以方法区、JVM内部处理或优化所需的内存(如JIT编译后的代码缓存)、每个类结构(如运行时常数池、字段和方法数据)以及方法和构造方法 的代码都在非堆内存中。

堆内存分配

JVM初始分配的内存由-Xms指定,默认是物理内存的1/64;JVM最大分配的内存由 -Xmx指定,默认是物理内存的1/4。默认空余堆内存小于40%时,JVM就会增大堆直到-Xmx的最大限制;空余堆内存大于70%时,JVM会减少堆 直到-Xms的最小限制。因此服务器一般设置-Xms、-Xmx相等以避免在每次GC 后调整堆的大小。

非堆内存分配

JVM使用-XX:PermSize设置非堆内存初始值,默认是物理内存的1/64;由XX:MaxPermSize设置最大非堆内存的大小,默认是物理内存的1/4。

JVM内存限制(最大值)

首先JVM内存限制于实际的最大物理内存(废话!呵呵),假设物理内存无限 大的话,JVM内存的最大值跟操作系统有很大的关系。简单的说就32位处理器虽然可控内存空间有4GB,但是具体的操作系统会给一个限制,这个限制一般是 2GB-3GB(一般来说Windows系统下为1.5G-2G,Linux系统下为2G-3G),而64bit以上的处理器就不会有限制了

以上内容来自这里

类的反射机制

在java中,类、接口、Enum等编译后,都会生成.class文件,用来记录每一个类的具体信息。在加载.class文件时,JVM会产生一个Class实例来代表该.class文件,Class实例记录了每一个类的所有信息。通多Class实例,我们就可以实现反射机制了。
可以通过三种方法来获得java.lang.Class的实例

  1. 通过对象的成员函数getClass()来获得,该函数为java.lang.Object类的public函数,由于每一个类都是继承Object的,所以每一个类都会有该函数
  2. 通过类名.class来获得。
  3. 通过Class.forName()来获得。Class.forName有两个版本,Class.forName("类名")Class.forName("类名", bool值是否加载初始化,类加载器)

对于基本类型,也可以使用对应打包类上加.TYPE来取得Class对象,例如:
使用Integer.TYPE可取得代表int基本类型的Class,如果需要取得代表Integer.class文档的Class,那么必须使用Integer.class.

拥有Class实例后,就可以通过Class实例所记录的信息来获得对应类的信息以及生成类实例。可以得到类的所有构造方法、成员函数(包括静态方法)以及成员属性(包括静态属性),还可以访问以及修改对象的私有成员属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package chb.test.reflect;

public class Student {
private int age;
private String name;
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}

public static void hi(int age,String name){
System.out.println("大家好,我叫"+name+",今年"+age+"岁");
}
}
调用方法

在方法调用中,参数类型必须正确,这里需要注意的是不能使用包装类替换基本类型,比如不能使用Integer.class代替int.class

1
2
3
4
5
6
7
8
Class cls = Class.forName("chb.test.reflect.Student");
Method m = cls.getDeclaredMethod("hi",new Class[]{int.class,String.class});
m.invoke(cls.newInstance(),20,"chb");

//static方法调用时,不必得到对象
Class cls = Class.forName("chb.test.reflect.Student");
Method staticMethod = cls.getDeclaredMethod("hi",int.class,String.class);
staticMethod.invoke(cls,20,"chb");//这里不需要newInstance

private的成员变量赋值

如果直接通过反射给类的private成员变量赋值,是不允许的,这时我们可以通过setAccessible方法解决。代码示例:

1
2
3
4
5
6
Class cls = Class.forName("chb.test.reflect.Student");
Object student = cls.newInstance();
Field field = cls.getDeclaredField("age");
field.setAccessible(true);//设置允许访问
field.set(student, 10);
System.out.println(field.get(student));

其实,在某些场合下(类中有get,set方法),可以先反射调用set方法,再反射调用get方法达到如上效果,代码示例:

1
2
3
4
5
6
7
8
Class cls = Class.forName("chb.test.reflect.Student");
Object student = cls.newInstance();

Method setMethod = cls.getDeclaredMethod("setAge",Integer.class);
setMethod.invoke(student, 15);//调用set方法

Method getMethod = cls.getDeclaredMethod("getAge");
System.out.println(getMethod.invoke(student));//再调用get方法

以上的代码来自这里

类的加载到执行

在命令行中输入java xxx指令后,java执行程序会在JRE安装目录中寻找JVM启动文件,如果在windows中,就是jvm.dll文件,启动JVM后,接着JVM产生Bootstrap Loader类加载器,Bootstrap Loader类加载器接着产生Extended Loader,并且设置该加载器的父加载器为Bootstrap Loader,接着有产生System Loader,并且设置其父加载器为Extended Loader.
在java中,除了Bootstrap Loader之外,其他的类加载器都有父加载器。Bootstrap由C语言编写,其他的由java语言编写。
三种类型的加载器的主要功能如下:

  1. 引导类加载器(bootstrap class loader):它用来加载 Java 的核心库,是用原生代码来实现的,并不继承自 java.lang.ClassLoader。
  2. 扩展类加载器(extensions class loader):它用来加载 Java 的扩展库。Java 虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java 类。
  3. 系统类加载器(system class loader):它根据 Java 应用的类路径(CLASSPATH)来加载 Java 类。一般来说,Java 应用的类都是由它来完成加载的。可以通过 ClassLoader.getSystemClassLoader()来获取它。

加载

当以上过程完成后,System Loader就开始加载运行类了,也就是在运行中,需要用到新的类时,在默认情况下就由System Loader来负责加载。每一个类加载器在加载类时,都会把加载工作交给其父类加载器来完成,一层一层的往上提交,如果父类加载器不能完成加载工作,才由当前的类加载器来完成加载工作。这就是所谓的”类加载代理模式”。之所以采用该模式主要是为了保证java核心库的类型安全。在java虚拟机中,判定两个类是否相同,Java 虚拟机不仅要看类的全名是否相同,还要看加载此类的类加载器是否一样。只有两者都相同的情况,才认为两个类是相同的。即便是同样的字节代码,被不同的类加载器加载之后所得到的类,也是不同的。比如一个 Java 类 com.example.Sample,编译之后生成了字节代码文件 Sample.class。两个不同的类加载器 ClassLoaderA和 ClassLoaderB分别读取了这个 Sample.class文件,并定义出两个 java.lang.Class类的实例来表示这个类。这两个实例是不相同的。对于 Java 虚拟机来说,它们是不同的类。试图对这两个类的对象进行相互赋值,会抛出运行时异常 ClassCastException。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package com.example;

public class Sample {
private Sample instance;

public void setSample(Object instance) {
this.instance = (Sample) instance;
}
}
//测试类是否相同
public void testClassIdentity() {
String classDataRootPath = "C:\\workspace\\Classloader\\classData";
FileSystemClassLoader fscl1 = new FileSystemClassLoader(classDataRootPath);
FileSystemClassLoader fscl2 = new FileSystemClassLoader(classDataRootPath);
String className = "com.example.Sample";
try {
Class<?> class1 = fscl1.loadClass(className);
Object obj1 = class1.newInstance();
Class<?> class2 = fscl2.loadClass(className);
Object obj2 = class2.newInstance();
Method setSampleMethod = class1.getMethod("setSample", java.lang.Object.class);
setSampleMethod.invoke(obj1, obj2);
} catch (Exception e) {
e.printStackTrace();
}
}

//运行错误输出
java.lang.reflect.InvocationTargetException
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
at java.lang.reflect.Method.invoke(Method.java:597)
at classloader.ClassIdentity.testClassIdentity(ClassIdentity.java:26)
at classloader.ClassIdentity.main(ClassIdentity.java:9)
Caused by: java.lang.ClassCastException: com.example.Sample
cannot be cast to com.example.Sample
at com.example.Sample.setSample(Sample.java:7)
... 6 more

所有 Java 应用都至少需要引用 java.lang.Object类,也就是说在运行的时候,java.lang.Object这个类需要被加载到 Java 虚拟机中。如果这个加载过程由 Java 应用自己的类加载器来完成的话,很可能就存在多个版本的 java.lang.Object类,而且这些类之间是不兼容的。通过代理模式,对于 Java 核心库的类的加载工作由引导类加载器来统一完成,保证了 Java 应用所使用的都是同一个版本的 Java 核心库的类,是互相兼容的。

不同的类加载器为相同名称的类创建了额外的名称空间。相同名称的类可以并存在 Java 虚拟机中,只需要用不同的类加载器来加载它们即可。不同类加载器加载的类之间是不兼容的,这就相当于在 Java 虚拟机内部创建了一个个相互隔离的 Java 类空间。

当类对应的.class文件加载到JVM后,会创建一个java.lang.Class对象,一个Class对象对应一个.class文件,主要记录该文件的关于类的所有信息。我们可以通过该对象的newInstance()函数来生成类的实例,这种情况只适合在类具有无参数构造函数的情况下。默认情况下,JVM只会用一个Class实例来代表一个.class文件(确切说,应该是通过同一类加载器载入的.class文件),每一个类的实例都会知道自己由哪一个Class实例生成,可以听过对象.getClass()或是类名.class或是Class.forName("类名")来获得类的Class实例。Class实例记录了类的所有信息,可以通过该实例来得到具体类的对象,java的反射机制就是通过Class来实现的。

链接

当类被加载后,系统就为之创建一个对应的Class对象,接着就会进入连接阶段。连接阶段会负责吧类的二进制数据合并到JRE中。类连接又可以分为如下三个阶段:

  1. 验证:检验被加载的类是否有正确的内部结构,并和其它类协调一致。

  2. 准备:负责为类的静态属性分配内存,并设置默认初始值。

  3. 解析:将类的二进制数据中的符号引用替换成直接引用。

初始化

JVM负责对类进行初始化,也就是对静态属性进行初始化。在Java类中,对静态属性指定初始值的方式有两种:(1)声明静态属性时指定初始值;(2)使用静态初始化块为静态属性指定初始值。
默认情况下都是在Class实例生成后,对类进行初始化。但是也可以对其推迟,直到需要生成类的实例时,才进行初始化,而且只在第一次生成类的实例前才执行初始化。Class.forName("类名", bool值初始化与否, 类加载器)可以自定义初始化的时间。
完成以上工作后,程序就可以继续执行了。

设计模式之单例模式

单例模式的定义:确保一个类只有一个实例,并提供一个全局访问点

经典单例模式

1
2
3
4
5
6
7
8
9
10
public class Singleton{
private static Singleton uniqueInstance; //属于类的,只能通过函数得到,也是唯一的单例对象
private Singleton(){}; //私有的构造函数,不允许外界直接实例化对象

public static Singleton getInstance(){ //获得单例对象的全局访问点
if(uniqueInstance == null)
uniqueInstance = new Singleton();
return uniqueInstance;
}
}

以上代码在单线程的环境下运行的很好,但是在多线程的情况下,就会出现问题。主要在于getInstance函数在多线程情况下会出现资源竞争,可以对getInstance函数变成同步的方法

1
2
3
4
5
public static synchronized Singleton getInstance(){    //获得单例对象的全局访问点
if(uniqueInstance == null)
uniqueInstance = new Singleton();
return uniqueInstance;
}

但是同步一个方法可能造成程序执行效率降低100倍,而且其实在getInstance函数中,并不是整一个方法都属于资源竞争的范围,只有uniqueInstance = new Singleton()语句才是,而且只有在单例对象第一次初始化的时候才会执行该语句,其余的都不会进入到该语句中,所以直接对整一个方法进行同步有点浪费。
可以双重检查加锁,在getInstance函数中减少同步

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Singleton{
//volatile 表明该变量是易变的,编译器不要对其进行优化,每一个反问的时候都要在内存中进行读取,不要存放在寄存器中
private static volatile Singleton uniqueInstance;

public static Singleton getInstance(){
if(uniqueInstance == null){
synchronized(Singleton.class){
//在执行到这里面是,并不知道uniqueInstance对象会不会被其他的线程改变,所以需要在检查一下
//以确保在null的情况下才实例化一个对象
//volatile关键字表明每一次对uniqueInstance变量的读取都是直接在内存中读的
if(uniqueInstance == null)
uniqueInstance = new Singleton();
}
}
return uniqueInstance;
}
}

以上代码可以在多线程的情况下保证代码的正确运行同时对程序执行效率没有影响,只有在第一次初始化是才会执行同步方法。

以上的单例程序都是在程序调用getInstance方法时,才会实例化Singleton对象的,这也就是所谓的”延迟实例化”,这样可以保证资源不被浪费。如果单例对象所占有的资源不大,那么也可以直接的实例化——“急切”实例化。

1
2
3
4
5
6
7
public class Singleton{
//在类加载的时候直接的实例化,不存在多线程的问题
private static Singleton uniqueInstance = new Singleton();
public static Singleton getInstance(){
return uniqueInstance;
}
}

在这个方法中,我们依赖JVM在加载这个类的时候就直接创建单例实例。JVM确保在任何线程反问uniqueInstance静态变量前,一定先创建此实例。

设计模式之观察者模式

观察者模式有由两部分组成:主题和观察者。主题负责生产数据,观察者通过订阅主题来获得需要观察的数据
观察者模式定义:定义了对象之间的一对多依赖,这样一来,当一个对象改变状态时,他的所有依赖者都收到通知并自动更新。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
//主题接口
public Interface Subject{
registerObserver(Observer);
removeObserver(Observer);
notifyObservers(); //有新数据的时候,调用该函数
}

//观察者接口
public Interface Observer{
//在主题有数据更新时,在notifyObservers函数里面,
//会对每一个已经注册的Observer调用该函数,实现对Observer的通知
void update();
}

//具体主题类
public class ASubject implements Subject{
public void registerObserver(Observer ob){...};
public void removeObserver(Observer ob){...};
public void notifyObservers(){ //有新数据的时候,调用该函数
for(Observer ob : Observers){
ob.update();
}
};
...
...
}

//具体的观察者类
public class AObserver implements Observer{
private Subject sbj; //对主题的应用,以实现对所感兴趣的主题的订阅与取消订阅
public void update(){...};
...
...
}

题外话

  1. “在Java里面参数传递都是按值传递”这句话的意思是:按值传递是传递的值的拷贝,按引用传递其实传递的是引用的地址值,所以统称按值传递。

  2. 在Java里面只有基本类型和按照下面这种定义方式的String是按值传递,其它的都是按引用传递。就是直接使用双引号定义的字符串方式:String str = “Java String”;

设计模式之工厂模式

工厂模式分为两种:工厂方法模式和抽象工厂模式
一个Pizaa店的订单:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class PizzaStore{
public Pizza orderPizza(String type){
Pizza p;
if("aa".equals(type))
p = new AAPizaa(); //AAPizza为Pizaa的子类
if("bb".equals(type))
p = BBPizaa(); //BBPizza为Pizaa的子类
if ...
...
...
return p;

}
}

简单工厂

简单工厂只是一种编程习惯,并不是真正的设计模式。他只是把类中实例化一个对象的工作给抽出来,用一个工厂类来负责对象的实例化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//实例化的工作被抽离出来
public class SimplePizzaFactory{
public SimplePizzaFactory(){};
public Pizza createPizza(String type){
if("aa".equals(type))
reutrn new AAPizza(); //AAPizza为Pizza的子类
if("bb".equals(type))
return BBPizza(); //BBPizza为Pizza的子类
if ...
...
}
}
public class PizzaStore{
SimplePizzaFactory spf;
public PizzaStore(SimplePizzaFactory spf){
this.spf = spf;
}
public Pizza orderPizza(String type){
Pizza p;
p = spf.createPizza(type);
...
...
return p;

}
}

工厂方法模式

工厂方法模式只是定义一个实例化子类的接口,具体子类的实例化由其派生类来决定实现
工厂方法模式定义:定义一个创建对象的接口,但由子类决定要实例化的类是哪一个类,工厂方法让类把实例化推迟到子类里。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public abstract class PizzaStore(){
abstract Pizza createPizza(String type); //工厂方法,具体pizza类的实例化由派生类来实现
public Pizza orderPizza(String type){
Pizza p;
p = createPizza(type);

}
}

//具体的对象实例化由派生类决定
public class AAPizzaStore() extends PizzaStore{
Pizza createPizza(String type){
System.out.println("I'm AAPizzaStore");
if("aa".equals(type))
reutrn new AA1Pizza(); //AA1Pizza为Pizza的子类

if("bb".equals(type))
return AA2Pizza(); //AA2Pizza为Pizza的子类

if ...
...
}

}

public class BBPizzaStore() extends PizzaStore{
Pizza createPizza(String type){
System.out.println("I'm BBPizzaStore");
if("aa".equals(type))
reutrn new BB1Pizza(); //BB1Pizza为Pizza的子类

if("bb".equals(type))
return BB2Pizza(); //BB2Pizza为Pizza的子类

if ...
...
}

}

抽象工厂模式

抽象工厂模式里面有很多个工厂方法,每一个工厂方法就是一个用来实例化类的接口
抽象工厂模式定义:提供一个接口,用于创建相关或依赖对象的家族,而不需要明确指定具体类
抽象工厂允许客户使用抽象的接口来创建一组相关的产品,而不需要知道实际产出来的具体产品是什么。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
//抽象工厂接口
public Interface PizzaIngredientFactory{
Dough createDough();
Sauce createSauce();
Cheese crateCheese();
Clam CreateClam();
...
}

//派生类
public class NYPizzaIngredientFactory implements PizzaIngredientFactory{
public Dough createDough(){
return NYDough(); //NYDough 为 Dough的子类
}

public Sauce createSauce(){
return NYSauce(); //NYSauce 为 Sauce的子类
}

public Cheese createCheese(){
return NYCheese(); //NYCheese 为 Cheese的子类
}

public Clam createClam(){
return NYClam(); //NYClam 为 Clam的子类
}
}


//派生类
public class ChicagoPizzaIngredientFactory implements PizzaIngredientFactory{
public Dough createDough(){
return ChicagoDough(); //ChicagoDough 为 Dough的子类
}

public Sauce createSauce(){
return ChicagoSauce(); //ChicagoSauce 为 Sauce的子类
}

public Cheese createCheese(){
return ChicagoCheese(); //ChicagoCheese 为 Cheese的子类
}

public Clam createClam(){
return ChicagoClam(); //ChicagoClam 为 Clam的子类
}
}

C++11常规特性之auto和decltype

在C++11之前,auto用来声明对象的存储期,修饰普通局部栈变量,是自动存储,这种对象会自动创建和销毁。在C++11新特性中,用来实现类型的推判。auto现在成了一个类型的占位符,通知编译器去根据初始化代码推断所声明变量的真实类型。特别是在循环遍历容器的时候,auto会显得很有用。

1
2
3
4
5
6
7
8
9
auto i = 42;
auto a = 3.14;
auto p = new Ptr();
auto i=0,&r=i;
auto a=r;//a为int,因为r是i的别名,i为int。
vector<int> v;
auto b = v.begin();
std::map<std::string, std::vector<int>> map;
for(auto it = begin(map); it != end(map); ++it) {};

需要注意的是,auto不能用来声明函数的返回值。但如果函数有一个尾随的返回类型时,auto是可以出现在函数声明中返回值位置。这种情况下,auto并不是告诉编译器去推断返回类型,而是指引编译器去函数的末端寻找返回值类型。在下面这个例子中,函数的返回值类型就是operator+操作符作用在T1、T2类型变量上的返回值类型。

1
2
3
4
5
6
template <typename T1, typename T2>
auto compose(T1 t1, T2 t2) -> decltype(t1 + t2)
{
return t1+t2;
}
auto v = compose(2, 3.14); // v's type is double

使用auto时,你只是需要一个变量的类型初始化。如果你需要一个类型不是一个变量,那么你需要用到decltype,例如返回类型。decltype是根据变量推导获取出变量的类型。目的是选择并返回操作数的数据类型,重要的是,在此过程中编译器分析表达式并得到它的类型,却不实际计算表达式的值。

1
2
3
4
5
6
7
8
9
decltype(f()) sum=x;  //sum的类型就是f返回值的类型, 但是这里不执行函数f()
auto x= 12;
auto y = 13;
typedef decltype(x*y) Type;
decltype(x*y) xy; //xy的类型为int
//decltype声明函数指针的时,关键是要记住decltype返回的是一个函数的类型的,因此要加上*声明符才能构成完整的函数指针的类型
int f(int a, int b){return a+b;};
decltype(f)* k = f; //直接decltype(f) k = f 是不可以的
k(1, 2);

如果decltype使用表达式的结果类型可以作为一条赋值语句的左值,那么decltype返回一个引用类型,例如解引用操作和变量加括号的类型。

1
2
3
4
5
6
int k=9;
decltype(*p) c=k; //c为int&,必须初始化
decltype((i)) d = k; //d为int&,必须初始化
decltype(++i) e = k //e为int&, 必须初始化
decltype(i++) f; //f 为int类型
tecltype(f=k) g //g为int&类型,必须初始化

auto和decltype的区别

const和引用

auto和const的推断与decltype不一样,对于auto,变量顶层的const会被忽略,只保留底层的const

1
2
3
4
5
6
7
8
onst int ci=i,&cr=i;
auto a=ci; //a为int(忽略顶层const)
auto b=cr; //b为int(忽略顶层const,cr是引用)
auto c=&i; //c为int *
auto d=&ci; //d是pointer to const int(&ci为底层const)
//要声明顶层const,前面要加上const关键字;要声明引用要加上&标识符
const auto f=ci; //ci的推演类型是int,f是const int
auto &g=ci;// g是一个绑定到ci的引用

对于decltype,其对const和应用的处理如下

1
2
3
4
5
6
const int ci=0;
decltype(ci) x=0; //x的类型是const int
int i=42,*p=&i,&r=i;
decltype(r+0) b; //b为int
const int &cj=ci;
decltype(cj) y=x; //y的类型是const int&,y绑定到x上

C++11常规特性之统一初始化和初始化器列表

在c++11以前,程序员,或者初学者经常会感到疑惑关于怎样去初始化一个变量或者是一个对象。
初始化经常使用括号,或者是使用大括号,或者是复赋值操作。
因为这个原因,c++11提出了统一初始化,以为着使用这初始化列表,下面的做法都是正确的。

1
2
3
4
int value[] {1 , 2 , 3};
std::vector<int> vi {2 , 3 , 4 , 56, 7};
std::vector<std::string> cities {"Berlin" , "New York " , "london " , "cairo"};
std::complex<double> c{4.0 , 3.0}; //相当于c(4.0 , 3.0);

一个初始化列表强制使用赋值操作, 也就是为每个变量设置一个默认的初始化值,被初始化为0(NULL 或者是 nullptr)
如下:

1
2
3
4
int i; //这是一个未定义的行为
int i{}; //i调用默认的构造函数为i赋值为0
int *p; //这是一个未定义的行为
int *p{} ;// p被初始化为一个nullptr

初始化类表不会进行隐式转换
例如:

1
2
3
4
5
6
int  x1(5.3); // 5
int x2 = 5.3 //5
int xi{5.0} //精确地 所以会出现error
int x4 = {5.3} // 精确地 所以会出现error
char ci{7};
char c9{9999}; //error 9999不合适是一个char类型

如果是自己想实现初始化列表构造函数,拷贝函数,赋值函数,需要包含initializer_list 这个头文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class MyClass{
public:
MyClass(int a):a_(a){
std::cout << "normal initializer list\n";
}

MyClass(std::initializer_list<int> a):b_(a) {
std::cout << "initializer list constructor\n";
}

MyClass(MyClass& my) {
std::cout << "copy constructor\n";
this->a_ = my.a_;
this->b_ = my.b_;
}

MyClass& operator=(MyClass& my) {
std::cout << "operator = constructor\n";
this->a_ = my.a_;
this->b_ = my.b_;
return *this;
}
private:
int a_;
std::initializer_list<int> b_;
};

下面是初始化以及输出结果:

1
2
3
4
5
6
7
8
9
MyClass ma{1};               // (a)
MyClass mb = {1, 2, 3}; // (b)
MyClass mc(2); // (c)
MyClass md = b; // (d)
MyClass me(c); // (e)
MyClass mf{e}; // (f)
auto l{2, 2, 3,3};
MyClass mh{l}; // (e)
ma = mb; // (h)

输出结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
initializer list constructor

initializer list constructor

normal constructor list

copy constructor

copy constructor

copy constructor

initializer list constructor

operator = constructor

C++11常规特性之右值引用和move语义

C++11加入了右值引用(rvalue reference)的概念(用&&标识),用来区分对左值和右值的引用。左值就是一个有名字的对象,而右值则是一个无名对象(临时对象)。move语义允许修改右值(以前右值被看作是不可修改的,等同于const T&类型)。
C++的class或者struct以前都有一些隐含的成员函数:默认构造函数(仅当没有显示定义任何其他构造函数时才存在),拷贝构造函数,析构函数还有拷贝赋值操作符。拷贝构造函数和拷贝赋值操作符提供bit-wise的拷贝(浅拷贝),也就是逐个bit拷贝对象。也就是说,如果你有一个类包含指向其他对象的指针,拷贝时只会拷贝指针的值而不会管指向的对象。在某些情况下这种做法是没问题的,但在很多情况下,实际上你需要的是深拷贝,也就是说你希望拷贝指针所指向的对象。而不是拷贝指针的值。这种情况下,你需要显示地提供拷贝构造函数与拷贝赋值操作符来进行深拷贝。

如果你用来初始化或拷贝的源对象是个右值(临时对象)会怎么样呢?你仍然需要拷贝它的值,但随后很快右值就会被释放。这意味着产生了额外的操作开销,包括原本并不需要的空间分配以及内存拷贝。

现在说说move constructor和move assignment operator。这两个函数接收T&&类型的参数,也就是一个右值。在这种情况下,它们可以修改右值对象,例如“偷走”它们内部指针所指向的对象。举个例子,一个容器的实现(例如vector或者queue)可能包含一个指向元素数组的指针。当用一个临时对象初始化一个对象时,我们不需要分配另一个数组,从临时对象中把值复制过来,然后在临时对象析构时释放它的内存。我们只需要将指向数组内存的指针值复制过来,由此节约了一次内存分配,一次元数组的复制以及后来的内存释放。

以下代码实现了一个简易的buffer。这个buffer有一个成员记录buffer名称(为了便于以下的说明),一个指针(封装在unique_ptr中)指向元素为T类型的数组,还有一个记录数组长度的变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
template <typename T>
class Buffer
{
std::string _name;
size_t _size;
std::unique_ptr<T[]> _buffer;

public:
// default constructor
Buffer():
_size(16),
_buffer(new T[16])
{}

// constructor
Buffer(const std::string& name, size_t size):
_name(name),
_size(size),
_buffer(new T[size])
{}

// copy constructor
Buffer(const Buffer& copy):
_name(copy._name),
_size(copy._size),
_buffer(new T[copy._size])
{
T* source = copy._buffer.get();
T* dest = _buffer.get();
std::copy(source, source + copy._size, dest);
}

// copy assignment operator
Buffer& operator=(const Buffer& copy)
{
if(this != ©)
{
_name = copy._name;

if(_size != copy._size)
{
_buffer = nullptr;
_size = copy._size;
_buffer = _size > 0 > new T[_size] : nullptr;
}

T* source = copy._buffer.get();
T* dest = _buffer.get();
std::copy(source, source + copy._size, dest);
}

return *this;
}

// move constructor
Buffer(Buffer&& temp):
_name(std::move(temp._name)),
_size(temp._size),
_buffer(std::move(temp._buffer))
{
temp._buffer = nullptr;
temp._size = 0;
}

// move assignment operator
Buffer& operator=(Buffer&& temp)
{
assert(this != &temp); // assert if this is not a temporary

_buffer = nullptr;
_size = temp._size;
_buffer = std::move(temp._buffer);

_name = std::move(temp._name);

temp._buffer = nullptr;
temp._size = 0;

return *this;
}
};

template <typename T>
Buffer<T> getBuffer(const std::string& name)
{
Buffer<T> b(name, 128);
return b;
}
int main()
{

Buffer<int> b1;
Buffer<int> b2("buf2", 64);
Buffer<int> b3 = b2;
Buffer<int> b4 = getBuffer<int>("buf4");
b1 = getBuffer<int>("buf5");
return 0;
}

默认的copy constructor以及copy assignment operator大家应该很熟悉了。C++11中新增的是move constructor以及move assignment operator,这两个函数根据上文所描述的move语义实现。如果你运行这段代码,你就会发现b4构造时,move constructor会被调用。同样,对b1赋值时,move assignment operator会被调用。原因就在于getBuffer()的返回值是一个临时对象——也就是右值。

你也许注意到了,move constuctor中当我们初始化变量name和指向buffer的指针时,我们使用了std::move。name实际上是一个string,std::string实现了move语义。std::unique_ptr也一样。但是如果我们写_name(temp._name),那么copy constructor将会被调用。不过对于_buffer来说不能这么写,因为std::unique_ptr没有copy constructor。但为什么std::string的move constructor此时没有被调到呢?这是因为虽然我们使用一个右值调用了Buffer的move constructor,但在这个构造函数内,它实际上是个左值。为什么?因为它是有名字的——“temp”。一个有名字的对象就是左值。为了再把它变为右值(以便调用move constructor)必须使用std::move。这个函数仅仅是把一个左值引用变为一个右值引用。

更新:虽然这个例子是为了说明如何实现move constructor以及move assignment operator,但具体的实现方式并不是唯一的。在本文的回复中Member 7805758同学提供了另一种可能的实现。为了方便查看,我把它也列在下面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
template <typename T>
class Buffer
{
std::string _name;
size_t _size;
std::unique_ptr<T[]> _buffer;

public:
// constructor
Buffer(const std::string& name = "", size_t size = 16):
_name(name),
_size(size),
_buffer(size? new T[size] : nullptr)
{}

// copy constructor
Buffer(const Buffer& copy):
_name(copy._name),
_size(copy._size),
_buffer(copy._size? new T[copy._size] : nullptr)
{
T* source = copy._buffer.get();
T* dest = _buffer.get();
std::copy(source, source + copy._size, dest);
}

// copy assignment operator
Buffer& operator=(Buffer copy)
{
swap(*this, copy);
return *this;
}

// move constructor
Buffer(Buffer&& temp):Buffer()
{
swap(*this, temp);
}

friend void swap(Buffer& first, Buffer& second) noexcept
{

using std::swap;
swap(first._name , second._name);
swap(first._size , second._size);
swap(first._buffer, second._buffer);
}
};

move语义不仅仅用于右值,也用于左值。标准库提供了std::move方法,将左值转换成右值。因此,对于swap函数,我们可以这样实现

1
2
3
4
5
6
7
template<class T>
void swap(T& a, T& b)
{
T temp(std::move(a));
a = std::move(b);
b = std::move(temp);
}

以上内容转自这里

C++11常规特性之lambda

很多语言都提供了 lambda 表达式,如 Python,Java 8。lambda 表达式可以方便地构造匿名函数,如果你的代码里面存在大量的小函数,而这些函数一般只被调用一次,那么不妨将他们重构成 lambda 表达式。举一个例子。标准 C++ 库中有一个常用算法的库,其中提供了很多算法函数,比如 sort() 和 find()。这些函数通常需要提供一个“谓词函数 predicate function”。所谓谓词函数,就是进行一个操作用的临时函数。比如 find() 需要一个谓词,用于查找元素满足的条件;能够满足谓词函数的元素才会被查找出来。这样的谓词函数,使用临时的匿名函数,既可以减少函数数量,又会让代码变得清晰易读。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/*
mutable:当capture为传值的时候,函数不能修改外部的局部变量,如果需要修改,可以使用该关键字,
但是由于是传值,即使修改,也不会影响到capture的变量;传引用的时候,是可以修改变量的
而且会影响到所capture的变量
exception:说明 lambda 表达式是否抛出异常(noexcept),以及抛出何种异常,类似于void f() throw(X, Y)。
attribute: 用来声明属性
returnType: lambda函数的返回类型,可以不需要,lambda可以根据返回表达式自己推导
mutable exception attribute三个属性可以省略

capture 指定了在可见域范围内 lambda 表达式的代码内可见得外部变量的列表,具体解释如下:
[a,&b] a变量以值的方式呗捕获,b以引用的方式被捕获。
[this] 以值的方式捕获 this 指针。
[&] 以引用的方式捕获所有的外部自动变量。
[=] 以值的方式捕获所有的外部自动变量。
[] 不捕获外部的任何变量。
*/
[ capture ] ( params ) mutable exception attribute -> returnType { body }

使用例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
std::cout << [](float f) { return std::abs(f); } (-3.5);
std::cout << [](float f) -> int { return std::abs(f); } (-3.5);

//当我们想引用一个 lambda 表达式时,我们可以使用auto关键字
auto lambda = [] () -> int { return val * 100; };
//auto关键字实际会将 lambda表达式转换成一种类似于std::function的内部类型
//(但并不是std::function类型,虽然与std::function“兼容”)。所以,我们也可以这么写:
std::function<int()> lambda = [] () -> int { return val * 100; };

//传值方式,改变capture变量
float f0 = 1.0;
std::cout << [=](float f) mutable { return f0 += std::abs(f); } (-3.5); //f0仍然是1.0

//对于[=]或[&]的形式,lambda 表达式可以直接使用 this 指针。
//但是,对于[]的形式,如果要使用 this 指针,必须显式传入:
[this]() { this->someFunc(); }();

C++11常规特性之noexcept

在异常处理的代码中,程序员有可能看到过如下的异常声明表达形式:

1
void excpt_func() throw(int, double) { ... }

在excpt_func函数声明之后,我们定义了一个动态异常声明throw(int, double),该声明指出了excpt_func可能抛出的异常的类型。事实上,该特性很少被使用,因此在C++11中被弃用了(参见附录B),而表示函数不会抛出异常的动态异常声明throw()也被新的noexcept异常声明所取代。

noexcept形如其名地,表示其修饰的函数不会抛出异常。不过与throw()动态异常声明不同的是,在C++11中如果noexcept修饰的函数抛出了异常,编译器可以选择直接调用std::terminate()函数来终止程序的运行,这比基于异常机制的throw()在效率上会高一些。这是因为异常机制会带来一些额外开销,比如函数抛出异常,会导致函数栈被依次地展开(unwind),并依帧调用在本帧中已构造的自动变量的析构函数等。

从语法上讲,noexcept修饰符有两种形式,一种就是简单地在函数声明后加上noexcept关键字。比如:

1
void excpt_func() noexcept;

另外一种则可以接受一个常量表达式作为参数,如下所示:

1
void excpt_func() noexcept (常量表达式);

常量表达式的结果会被转换成一个bool类型的值。该值为true,表示函数不会抛出异常,反之,则有可能抛出异常。这里,不带常量表达式的noexcept相当于声明了noexcept(true),即不会抛出异常。

在通常情况下,在C++11中使用noexcept可以有效地阻止异常的传播与扩散。我们可以看看下面这个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <iostream>
using namespace std;
void Throw() { throw 1; }
void NoBlockThrow() { Throw(); }
void BlockThrow() noexcept { Throw(); }

int main() {
try {
Throw();
}
catch(...) {
cout << "Found throw." << endl; // Found throw.
}

try {
NoBlockThrow();
}
catch(...) {
cout << "Throw is not blocked." << endl; // Throw is not blocked.
}

try {
BlockThrow(); // terminate called after throwing an instance of 'int'
}
catch(...) {
cout << "Found throw 1." << endl;
}
}
// 编译选项:g++ -std=c++11 2-6-1.cpp

在代码清单2-12中,我们定义了Throw函数,该函数的唯一作用是抛出一个异常。而NoBlockThrow是一个调用Throw的普通函数,BlockThrow则是一个noexcept修饰的函数。从main的运行中我们可以看到,NoBlockThrow会让Throw函数抛出的异常继续抛出,直到main中的catch语句将其捕捉。而BlockThrow则会直接调用std::terminate中断程序的执行,从而阻止了异常的继续传播。从使用效果上看,这与C++98中的throw()是一样的。
而noexcept作为一个操作符时,通常可以用于模板。比如:

1
2
template <class T>
void fun() noexcept(noexcept(T())) {}

这里,fun函数是否是一个noexcept的函数,将由T()表达式是否会抛出异常所决定。这里的第二个noexcept就是一个noexcept操作符。当其参数是一个有可能抛出异常的表达式的时候,其返回值为false,反之为true(实际noexcept参数返回false还包括一些情况,这里就不展开讲了)。这样一来,我们就可以使模板函数根据条件实现noexcept修饰的版本或无noexcept修饰的版本。从泛型编程的角度看来,这样的设计保证了关于“函数是否抛出异常”这样的问题可以通过表达式进行推导。因此这也可以视作C++11为了更好地支持泛型编程而引入的特性。

虽然noexcept修饰的函数通过std::terminate的调用来结束程序的执行的方式可能会带来很多问题,比如无法保证对象的析构函数的正常调用,无法保证栈的自动释放等,但很多时候,“暴力”地终止整个程序确实是很简单有效的做法。事实上,noexcept被广泛地、系统地应用在C++11的标准库中,用于提高标准库的性能,以及满足一些阻止异常扩散的需求。

比如在C++98中,存在着使用throw()来声明不抛出异常的函数。

1
2
3
4
5
6
template<class T> class A {
public:
static constexpr T min() throw() { return T(); }
static constexpr T max() throw() { return T(); }
static constexpr T lowest() throw() { return T(); }
...

而在C++11中,则使用noexcept来替换throw()。

1
2
3
4
5
6
template<class T> class A {
public:
static constexpr T min() noexcept { return T(); }
static constexpr T max() noexcept { return T(); }
static constexpr T lowest() noexcept { return T(); }
...

又比如,在C++98中,new可能会包含一些抛出的std::bad_alloc异常。

1
2
void* operator new(std::size_t) throw(std::bad_alloc);
void* operator new[](std::size_t) throw(std::bad_alloc);

而在C++11中,则使用noexcept(false)来进行替代。

1
2
void* operator new(std::size_t) noexcept(false);
void* operator new[](std::size_t) noexcept(false);

当然,noexcept更大的作用是保证应用程序的安全。比如一个类析构函数不应该抛出异常,那么对于常被析构函数调用的delete函数来说,C++11默认将delete函数设置成noexcept,就可以提高应用程序的安全性。

1
2
void operator delete(void*) noexcept;
void operator delete[](void*) noexcept;

而同样出于安全考虑,C++11标准中让类的析构函数默认也是noexcept(true)的。当然,如果程序员显式地为析构函数指定了noexcept,或者类的基类或成员有noexcept(false)的析构函数,析构函数就不会再保持默认值。我们可以看看下面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#include <iostream>
using namespace std;

struct A {
~A() { throw 1; }
};

struct B {
~B() noexcept(false) { throw 2; }
};

struct C {
B b;
};

int funA() { A a; }
int funB() { B b; }
int funC() { C c; }

int main() {
try {
funB();
}
catch(...){
cout << "caught funB." << endl; // caught funB.
}

try {
funC();
}
catch(...){
cout << "caught funC." << endl; // caught funC.
}

try {
funA(); // terminate called after throwing an instance of 'int'
}
catch(...){
cout << "caught funA." << endl;
}
}
// 编译选项:g++ -std=c++11 2-6-2.cpp

在代码中,无论是析构函数声明为noexcept(false)的类B,还是包含了B类型成员的类C,其析构函数都是可以抛出异常的。只有什么都没有声明的类A,其析构函数被默认为noexcept(true),从而阻止了异常的扩散。这在实际的使用中,应该引起程序员的注意。

C++11常规特性之其他一些新特性

C++11有很多的新特性,这里只记录一下一些比较会常用到的新特性,有:auto和decltype、nullptr、noexcept、lambda、基于范围的for语句、初始化器列表、右值引用和move语义、constexpr、override和final、强类型枚举(Strong-type enums)、智能指针(Smart Pointers)、非成员begin()和end()以及static_assert和type traits

nullptr

以前都是用0来表示空指针的,但由于0可以被隐式类型转换为整形,这就会存在一些问题。关键字nullptr是std::nullptr_t类型的值,用来指代空指针。nullptr和任何指针类型以及类成员指针类型的空值之间可以发生隐式类型转换,同样也可以隐式转换为bool型(取值为false)。但是不存在到整形的隐式类型转换。但是为了向前兼容,0仍然是个合法的空指针值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void foo(int* p) {}

void bar(std::shared_ptr<int> p) {}

int* p1 = NULL;
int* p2 = nullptr;
if(p1 == p2)
{
}

foo(nullptr);
bar(nullptr);

bool f = nullptr;
int i = nullptr; // error: A native nullptr can only be converted to bool or, using reinterpret_cast, to an integral type

Range-based for loops (基于范围的for循环)

为了在遍历容器时支持”foreach”用法,C++11扩展了for语句的语法。用这个新的写法,可以遍历C类型的数组、初始化列表以及任何重载了非成员的begin()和end()函数的类型。

1
2
3
4
5
6
7
8
9
10
for (const auto x : { 1,2,3,5,8,13,21,34 }) cout << x << '\n';

void f(vector<double>& v)
{

for (auto x : v) cout << x << '\n';
for (auto& x : v) ++x; // using a reference to allow us to change the value
}

int arr[] = {1,2,3,4,5};
for(int& e : arr) {e=e*e;};

constexpr和const

const并未区分出编译期常量和运行期常量
constexpr限定在了编译期常量

1
2
3
constexpr int mf = 20; // 常量表达式
constexpr int limit = mf + 1; // 常量表达式
constexpr int sz = size(); // 如果size()是常量表达式则编译通过,否则报错

constexpr修饰的函数,返回值不一定是编译期常量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>
#include <array>
using namespace std;

constexpr int foo(int i)
{

return i + 5;
}

int main()
{

int i = 10;
std::array<int, foo(5)> arr; // OK

foo(i); // Call is Ok

// But...
std::array<int, foo(i)> arr1; // Error

}

constexpr修饰的函数,简单的来说,如果其传入的参数可以在编译时期计算出来,那么这个函数就会产生编译时期的值。但是,传入的参数如果不能在编译时期计算出来,那么constexpr修饰的函数就和普通函数一样了。不过,我们不必因此而写两个版本,所以如果函数体适用于constexpr函数的条件,可以尽量加上constexpr。

而检测constexpr函数是否产生编译时期值的方法很简单,就是利用std::array需要编译期常值才能编译通过的小技巧。这样的话,即可检测你所写的函数是否真的产生编译期常值了。

以上内容来自知乎

通常,我们希望编译时期计算可以保护全局或者名字空间内的对象,对名字空间内的对象,我们希望它保存在只读空间内。
对于那些构造函数比较简单,可以成为常量表达式(也就是可以使用constexpr进行修饰)的对象可以做到这一点

1
2
3
4
5
6
7
8
struct Point {
int x,y;
constexpr Point(int xx, int yy) : x(xx), y(yy) { }
};
constexpr Point origo(0,0);
constexpr int z = origo.x;
constexpr Point a[] = {Point(0,0), Point(1,1), Point(2,2) };
constexpr int x = a[1].x; // x becomes 1

  1. const的主要功能是修饰一个对象而不是通过一个接口(即使对象很容易通过其他接口修改)。只不过声明一个对象常量为编译器提供了优化的机会。特别是,如果一个声明了一个对象常量而他的地址没有取到,编译器通常可以在编译时对他进行初始化(尽管这不是肯定的)保证这个对象在他的列表里而不是把它添加到生成代码里。
  2. constexpr的主要功能可以在编译时计算表达式的值进行了范围扩展,这是一种计算安全而且可以用在编译时期(如初始化枚举或者整体模板参数)。constexpr声明对象可以在初始化编译的时候计算出结果来。他们基本上只保存在编译器的列表,如果需要的话会释放到生成的代码里。

以上内容来自知乎

override和final

我总觉得C++中虚函数的设计很差劲,因为时至今日仍然没有一个强制的机制来标识虚函数会在派生类里被改写。vitual关键字是可选的,这使得阅读代码变得很费劲。因为可能需要追溯到继承体系的源头才能确定某个方法是否是虚函数。为了增加可读性,我总是在派生类里也写上virtual关键字,并且也鼓励大家都这么做。即使这样,仍然会产生一些微妙的错误。看下面这个例子:

1
2
3
4
5
6
7
8
9
10
11
class B
{
public:
virtual void f(short) {std::cout << "B::f" << std::endl;}
};

class D : public B
{
public:
virtual void f(int) {std::cout << "D::f" << std::endl;}
};

D::f 按理应当重写 B::f。然而二者的声明是不同的,一个参数是short,另一个是int。因此D::f(原文为B::f,可能是作者笔误——译者注)只是拥有同样名字的另一个函数(重载)而不是重写。当你通过B类型的指针调用f()可能会期望打印出D::f,但实际上则会打出 B::f 。

另一个很微妙的错误情况:参数相同,但是基类的函数是const的,派生类的函数却不是。

1
2
3
4
5
6
7
8
9
10
11
class B
{
public:
virtual void f(int) const {std::cout << "B::f " << std::endl;}
};

class D : public B
{
public:
virtual void f(int) {std::cout << "D::f" << std::endl;}
};

同样,这两个函数是重载而不是重写,所以你通过B类型指针调用f()将打印B::f,而不是D::f。

幸运的是,现在有一种方式能描述你的意图。新标准加入了两个新的标识符(不是关键字)::

  1. override,表示函数应当重写基类中的虚函数。
  2. final,表示派生类不应当重写这个虚函数。
    第一个的例子如下:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    class B
    {
    public:
    virtual void f(short) {std::cout << "B::f" << std::endl;}
    };

    class D : public B
    {
    public:
    virtual void f(int) override {std::cout << "D::f" << std::endl;}
    };

现在这将触发一个编译错误(后面那个例子,如果也写上override标识,会得到相同的错误提示):

1
'D::f' : method with override specifier 'override' did not override any base class methods

另一方面,如果你希望函数不要再被派生类进一步重写,你可以把它标识为final。可以在基类或任何派生类中使用final。在派生类中,可以同时使用override和final标识。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class B
{
public:
virtual void f(int) {std::cout << "B::f" << std::endl;}
};


class D : public B
{
public:
virtual void f(int) override final {std::cout << "D::f" << std::endl;}
};

class F : public D
{
public:
virtual void f(int) override {std::cout << "F::f" << std::endl;}
};

被标记成final的函数将不能再被F::f重写。

Strongly-typed enums 强类型枚举

传统的C++枚举类型存在一些缺陷:它们会将枚举常量暴露在外层作用域中(这可能导致名字冲突,如果同一个作用域中存在两个不同的枚举类型,但是具有相同的枚举常量就会冲突),而且它们会被隐式转换为整形,无法拥有特定的用户定义类型。

在C++11中通过引入了一个称为强类型枚举的新类型,修正了这种情况。强类型枚举由关键字enum class标识。它不会将枚举常量暴露到外层作用域中,也不会隐式转换为整形,并且拥有用户指定的特定类型(传统枚举也增加了这个性质)。

1
2
enum class Options {None, One, All};
Options o = Options::All;

在标准C++中,枚举类型不是类型安全的。枚举类型被视为整数,这使得两种不同的枚举类型之间可以进行比较。C++03 唯一提供的安全机制是一个整数或一个枚举型值不能隐式转换到另一个枚举别型。 此外,枚举所使用整数类型及其大小都由实现方法定义,皆无法明确指定。 最后,枚举的名称全数暴露于一般范围中,因此C++03两个不同的枚举,不可以有相同的枚举名。 (好比 enum Side{ Right, Left }; 和 enum Thing{ Wrong, Right }; 不能一起使用。)

1
2
3
4
5
6
7
enum class Enumeration
{
Val1,
Val2,
Val3 = 100,
Val4 /* = 101 */,
};

此种枚举为类型安全的。枚举类型不能隐式地转换为整数;也无法与整数数值做比较。 (表示式 Enumeration::Val4 == 101 会触发编译期错误)。

枚举类型所使用类型必须显式指定。在上面的示例中,使用的是默认类型 int,但也可以指定其他类型:

1
enum class Enum2 : unsigned int {Val1, Val2};

枚举类型的语汇范围(scoping)定义于枚举类型的名称范围中。 使用枚举类型的枚举名时,必须明确指定其所属范围。 由前述枚举类型 Enum2 为例,Enum2::Val1是有意义的表示法, 而单独的 Val1 则否。

此外,C++11 允许为传统的枚举指定使用类型:

1
enum Enum3 : unsigned long {Val1 = 1, Val2};

枚举名 Val1 定义于 Enum3 的枚举范围中(Enum3::Val1),但为了兼容性, Val1 仍然可以于一般的范围中单独使用。

在 C++11 中,枚举类型的前置声明 (forward declaration) 也是可行的,只要使用可指定类型的新式枚举即可。 之前的 C++ 无法写出枚举的前置声明,是由于无法确定枚举参数所占的空间大小, C++11 解决了这个问题:

1
2
3
4
5
enum Enum1;                     // 不合法的 C++ 與 C++11; 無法判別大小
enum Enum2 : unsigned int; // 合法的 C++11
enum class Enum3; // 合法的 C++11,列舉類別使用預設型別 int
enum class Enum4: unsigned int; // 合法的 C++11
enum Enum2 : unsigned short; // 不合法的 C++11,Enum2 已被聲明為 unsigned int

static_assert和 type traits

static_assert提供一个编译时的断言检查。如果断言为真,什么也不会发生。如果断言为假,编译器会打印一个特殊的错误信息。是在编译的时候进行断言,所以在实时编译的环境下编辑代码的时候,如果断言为假的话,就会直接提示错误

1
2
3
4
5
6
const int sj = 4;
static_assert(sj<9, "Size is too small"); //OK and assert is true

int a = 4;
// error 因为是在编译期的断言,所以在编译期必须能够对断言的内容进行确定,由于a是运行时动态确定的,所以这里编译错误
static_assert(a<9, "Size is too small");

static_assert和type traits一起使用能发挥更大的威力。type traits是一些class,在编译时提供关于类型的信息。在头文件中可以找到它们。这个头文件中有好几种class: helper class,用来产生编译时常量。type traits class,用来在编译时获取类型信息,还有就是type transformation class,他们可以将已存在的类型变换为新的类型。

下面这段代码原本期望只做用于整数类型。

1
2
3
4
5
template <typename T1, typename T2>
auto add(T1 t1, T2 t2) -> decltype(t1 + t2)
{
return t1 + t2;
}

但是如果有人写出如下代码,编译器并不会报错

1
2
std::cout << add(1, 3.14) << std::endl;
std::cout << add("one", 2) << std::endl;

程序会打印出4.14和”e”。但是如果我们加上编译时断言,那么以上两行将产生编译错误。

1
2
3
4
5
6
7
8
template <typename T1, typename T2>
auto add(T1 t1, T2 t2) -> decltype(t1 + t2)
{
static_assert(std::is_integral<T1>::value, "Type T1 must be integral");
static_assert(std::is_integral<T2>::value, "Type T2 must be integral");

return t1 + t2;
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
error C2338: Type T2 must be integral
see reference to function template instantiation 'T2 add<int,double>(T1,T2)' being compiled
with
[
T2=double,
T1=int
]
error C2338: Type T1 must be integral
see reference to function template instantiation 'T1 add<const char*,int>(T1,T2)' being compiled
with
[
T1=const char *,
T2=int
]

套接字

消息队列、信号灯、共享内存等,都是基于Sys V的IPC机制进行讨论的,它们的应用局限在单一计算机内的进程间通信;基于BSD套接口不仅可以实现单机内的进程间通信,还可以实现不同计算机进程之间的通信。

一个套接口可以看作是进程间通信的端点(endpoint),每个套接口的名字都是唯一的(唯一的含义是不言而喻的),其他进程可以发现、连接并且与之通信。通信域用来说明套接口通信的协议,不同的通信域有不同的通信协议以及套接口的地址结构等等,因此,创建一个套接口时,要指明它的通信域。比较常见的是unix域套接口(采用套接口机制实现单机内的进程间通信)及网际通信域。

1、背景知识

linux目前的网络内核代码主要基于伯克利的BSD的unix实现,整个结构采用的是一种面向对象的分层机制。层与层之间有严格的接口定义。这里我们引用[1]中的一个图表来描述linux支持的一些通信协议:

我们这里只关心IPS,即因特网协议族,也就是通常所说的TCP/IP网络。我们这里假设读者具有网络方面的一些背景知识,如了解网络的分层结构,通常所说的7层结构;了解IP地址以及路由的一些基本知识。

目前linux网络API是基于BSD套接口的(系统V提供基于流I/O子系统的用户接口,但是linux内核目前不支持流I/O子系统)。套接口可以说是网络编程中一个非常重要的概念,linux以文件的形式实现套接口,与套接口相应的文件属于sockfs特殊文件系统,创建一个套接口就是在sockfs中创建一个特殊文件,并建立起为实现套接口功能的相关数据结构。换句话说,对每一个新创建的BSD套接口,linux内核都将在sockfs特殊文件系统中创建一个新的inode。描述套接口的数据结构是socket,将在后面给出。

2、重要数据结构

下面是在网络编程中比较重要的几个数据结构,读者可以在后面介绍编程API部分再回过头来了解它们。

(1)表示套接口的数据结构struct socket

套接口是由socket数据结构代表的,形式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct socket
{
socket_state state; /* 指明套接口的连接状态,一个套接口的连接状态可以有以下几种
套接口是空闲的,还没有进行相应的端口及地址的绑定;还没有连接;正在连接中;已经连接;正在解除连接。 */

unsigned long flags;
struct proto_ops ops; /* 指明可对套接口进行的各种操作 */
struct inode inode; /* 指向sockfs文件系统中的相应inode */
struct fasync_struct *fasync_list; /* Asynchronous wake up list */
struct file *file; /* 指向sockfs文件系统中的相应文件 */
struct sock sk; /* 任何协议族都有其特定的套接口特性,该域就指向特定协议族的套接口对
象。 */

wait_queue_head_t wait;
short type;
unsigned char passcred;
};

(2)描述套接口通用地址的数据结构struct sockaddr

由于历史的缘故,在bind、connect等系统调用中,特定于协议的套接口地址结构指针都要强制转换成该通用的套接口地址结构指针。结构形式如下:

1
2
3
4
struct sockaddr {
sa_family_t sa_family; /* address family, AF_xxx */
char sa_data[14]; /* 14 bytes of protocol address */
};

(3)描述因特网地址结构的数据结构struct sockaddr_in(这里局限于IP4):

1
2
3
4
5
6
7
8
9
10
11
struct sockaddr_in
{
__SOCKADDR_COMMON (sin_); /* 描述协议族 */
in_port_t sin_port; /* 端口号 */
struct in_addr sin_addr; /* 因特网地址 */
/* Pad to size of `struct sockaddr'. */
unsigned char sin_zero[sizeof (struct sockaddr) -
__SOCKADDR_COMMON_SIZE -
sizeof (in_port_t) -
sizeof (struct in_addr)];
};

一般来说,读者最关心的是前三个域,即通信协议、端口号及地址。

3、套接口编程的几个重要步骤:

(1)创建套接口,由系统调用socket实现:

1
int socket( int domain, int type, int ptotocol);

参数domain指明通信域,如PF_UNIX(unix域),PF_INET(IPv4),PF_INET6(IPv6)等;type指明通信类型,如SOCK_STREAM(面向连接方式)、SOCK_DGRAM(非面向连接方式)等。一般来说,参数protocol可设置为0,除非用在原始套接口上(原始套接口有一些特殊功能,后面还将介绍)。

注:socket()系统调用为套接口在sockfs文件系统中分配一个新的文件和dentry对象,并通过文件描述符把它们与调用进程联系起来。进程可以像访问一个已经打开的文件一样访问套接口在sockfs中的对应文件。但进程绝不能调用open()来访问该文件(sockfs文件系统没有可视安装点,其中的文件永远不会出现在系统目录树上),当套接口被关闭时,内核会自动删除sockfs中的inodes。

(2)绑定地址

根据传输层协议(TCP、UDP)的不同,客户机及服务器的处理方式也有很大不同。但是,不管通信双方使用何种传输协议,都需要一种标识自己的机制。

通信双方一般由两个方面标识:地址和端口号(通常,一个IP地址和一个端口号常常被称为一个套接口)。根据地址可以寻址到主机,根据端口号则可以寻址到主机提供特定服务的进程,实际上,一个特定的端口号代表了一个提供特定服务的进程。

对于使用TCP传输协议通信方式来说,通信双方需要给自己绑定一个唯一标识自己的套接口,以便建立连接;对于使用UDP传输协议,只需要服务器绑定一个标识自己的套接口就可以了,用户则不需要绑定(在需要时,如调用connect时[注1],内核会自动分配一个本地地址和本地端口号)。绑定操作由系统调用bind()完成:

1
int bind( int sockfd, const struct sockaddr * my_addr, socklen_t my_addr_len)

第二个参数对于Ipv4来说,实际上需要填充的结构是struct sockaddr_in,前面已经介绍了该结构。这里只想强调该结构的第一个域,它表明该套接口使用的通信协议,如AF_INET。联系socket系统调用的第一个参数,读者可能会想到PF_INET与AF_INET究竟有什么不同?实际上,原来的想法是每个通信域(如PF_INET)可能对应多个协议(如AFINET),而事实上支持多个协议的通信域一直没有实现。因此,在linux内核中,AF与PF_被定义为同一个常数,因此,在编程时可以不加区分地使用他们。

注1:在采用非面向连接通信方式时,也会用到connect()调用,不过与在面向连接中的connect()调用有本质的区别:在非面向连接通信中,connect调用只是先设置一下对方的地址,内核为本地套接口记下对方的地址,然后采用send()来发送数据,这样避免每次发送时都要提供相同的目的地址。其中的connect()调用不涉及握手过程;而在面向连接的通信方式中,connect()要完成一个严格的握手过程。

(3)请求建立连接(由TCP客户发起)

对于采用面向连接的传输协议TCP实现通信来说,一个比较重要的步骤就是通信双方建立连接(如果采用udp传输协议则不需要),由系统调用connect()完成:

1
int connect( int sockfd, const struct sockaddr * servaddr, socklen_t addrlen)

第一个参数为本地调用socket后返回的描述符,第二个参数为服务器的地址结构指针。connect()向指定的套接口请求建立连接。

注:与connect()相对应,在服务器端,通过系统调用listen(),指定服务器端的套接口为监听套接口,监听每一个向服务器套接口发出的连接请求,并通过握手机制建立连接。内核为listen()维护两个队列:已完成连接队列和未完成连接队列。

(4)接受连接请求(由TCP服务器端发起)

服务器端通过监听套接口,为所有连接请求建立了两个队列:已完成连接队列和未完成连接队列(每个监听套接口都对应这样两个队列,当然,一般服务器只有一个监听套接口)。通过accept()调用,服务器将在监听套接口的已连接队列头中,返回用于代表当前连接的套接口描述字。

1
int accept( int sockfd, struct sockaddr * cliaddr, socklen_t * addrlen)

第一个参数指明哪个监听套接口,一般是由listen()系统调用指定的(由于每个监听套接口都对应已连接和未连接两个队列,因此它的内部机制实质是通过sockfd指定在哪个已连接队列头中返回一个用于当前客户的连接,如果相应的已连接队列为空,accept进入睡眠)。第二个参数指明客户的地址结构,如果对客户的身份不感兴趣,可指定其为空。

注:对于采用TCP传输协议进行通信的服务器和客户机来说,一定要经过客户请求建立连接,服务器接受连接请求这一过程;而对采用UDP传输协议的通信双方则不需要这一步骤。

(5)通信

客户机可以通过套接口接收服务器传过来的数据,也可以通过套接口向服务器发送数据。前面所有的准备工作(创建套接口、绑定等操作)都是为这一步骤准备的。

常用的从套接口中接收数据的调用有:recv、recvfrom、recvmsg等,常用的向套接口中发送数据的调用有send、sendto、sendmsg等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
int recv(int s, void *
buf, size_t
len, int
flags)

int recvfrom(int s, void *
buf, size_t
len, int
flags, struct sockaddr *
from, socklen_t *
fromlen)

int recvmsg(int s, struct msghdr *
msg, int
flags)

int send(int s,const void *
msg, size_t
len, int
flags)

int sendto(int s, const void *
msg, size_t
len, int
flags const struct sockaddr *
to, socklen_t
tolen)

int sendmsg(int s, const struct msghdr *
msg, int
flags)

这里不再对这些调用作具体的说明,只想强调一下,recvfrom()以及recvmsg()可用于面向连接的套接口,也可用于面向非连接的套接口;而recv()一般用于面向连接的套接口。另外,在调用了connect()之后,就应给调用send()而不是sendto()了,因为调用了connect之后,目标就已经确定了。

前面讲到,socket()系统调用返回套接口描述字,实际上它是一个文件描述符。所以,可以对套接口进行通常的读写操作,即使用read()及write()方法。在实际应用中,由于面向连接的通信(采用TCP传输协议)是可靠的,同时又保证字节流原有的顺序,所以更适合用read及write方法。而非面向连接的通信(采用UDP传输协议)是不可靠的,字节流也不一定保持原有的顺序,所以一般不宜用read及write方法。

(6)通信的最后一步是关闭套接口

由close()来完成此项功能,它唯一的参数是套接口描述字,不再赘述。

4、典型调用代码:

到处可以发现基于套接口的客户机及服务器程序,这里不再给出完整的范例代码,只是给出它们的典型调用代码,并给出简要说明。

(1)典型的TCP服务器代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
... ...
int listen_fd, connect_fd;
struct sockaddr_in serv_addr, client_addr;
... ...
listen_fd = socket ( PF_INET, SOCK_STREAM, 0 );

/* 创建网际Ipv4域的(由PF_INET指定)面向连接的(由SOCK_STREAM指定,
如果创建非面向连接的套接口则指定为SOCK_DGRAM)
的套接口。第三个参数0表示由内核确定缺省的传输协议,
对于本例,由于创建的是可靠的面向连接的基于流的套接口,
内核将选择TCP作为本套接口的传输协议) */


bzero( &serv_addr, sizeof(serv_addr) );
serv_addr.sin_family = AF_INET ; /* 指明通信协议族 */
serv_addr.sin_port = htons( 49152 ) ; /* 分配端口号 */
inet_pton(AF_INET, " 192.168.0.11", &serv_addr.sin_sddr) ;
/* 分配地址,把点分十进制IPv4地址转化为32位二进制Ipv4地址。 */
bind( listen_fd, (struct sockaddr*) serv_addr, sizeof ( struct sockaddr_in )) ;
/* 实现绑定操作 */
listen( listen_fd, max_num) ;
/* 套接口进入侦听状态,max_num规定了内核为此套接口排队的最大连接个数 */
for( ; ; ) {
... ...
connect_fd = accept( listen_fd, (struct sockaddr*)client_addr, &len ) ; /* 获得连接fd. */
... ... /* 发送和接收数据 */
}

注:端口号的分配是有一些惯例的,不同的端口号对应不同的服务或进程。比如一般都把端口号21分配给FTP服务器的TCP/IP实现。端口号一般分为3段,0-1023(受限的众所周知的端口,由分配数值的权威机构IANA管理),1024-49151(可以从IANA那里申请注册的端口),49152-65535(临时端口,这就是为什么代码中的端口号为49152)。

对于多字节整数在内存中有两种存储方式:一种是低字节在前,高字节在后,这样的存储顺序被称为低端字节序(little-endian);高字节在前,低字节在后的存储顺序则被称为高端字节序(big-endian)。网络协议在处理多字节整数时,采用的是高端字节序,而不同的主机可能采用不同的字节序。因此在编程时一定要考虑主机字节序与网络字节序间的相互转换。这就是程序中使用htons函数的原因,它返回网络字节序的整数。

(2)典型的TCP客户代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
... ...
int socket_fd;
struct sockaddr_in serv_addr ;
... ...
socket_fd = socket ( PF_INET, SOCK_STREAM, 0 );
bzero( &serv_addr, sizeof(serv_addr) );
serv_addr.sin_family = AF_INET ; /* 指明通信协议族 */
serv_addr.sin_port = htons( 49152 ) ; /* 分配端口号 */
inet_pton(AF_INET, " 192.168.0.11", &serv_addr.sin_sddr) ;
/* 分配地址,把点分十进制IPv4地址转化为32位二进制Ipv4地址。 */
connect( socket_fd, (struct sockaddr*)serv_addr, sizeof( serv_addr ) ) ; /* 向服务器发起连接请求 */
... ... /* 发送和接收数据 */
... ...

对比两段代码可以看出,许多调用是服务器或客户机所特有的。另外,对于非面向连接的传输协议,代码还有简单些,没有连接的发起请求和接收请求部分。

5、网络编程中的其他重要概念

下面列出了网络编程中的其他重要概念,基本上都是给出这些概念能够实现的功能,读者在编程过程中如果需要这些功能,可查阅相关概念。

(1)、I/O复用的概念

I/O复用提供一种能力,这种能力使得当一个I/O条件满足时,进程能够及时得到这个信息。I/O复用一般应用在进程需要处理多个描述字的场合。它的一个优势在于,进程不是阻塞在真正的I/O调用上,而是阻塞在select()调用上,select()可以同时处理多个描述字,如果它所处理的所有描述字的I/O都没有处于准备好的状态,那么将阻塞;如果有一个或多个描述字I/O处于准备好状态,则select()不阻塞,同时会根据准备好的特定描述字采取相应的I/O操作。

(2)、Unix通信域

前面主要介绍的是PF_INET通信域,实现网际间的进程间通信。基于Unix通信域(调用socket时指定通信域为PF_LOCAL即可)的套接口可以实现单机之间的进程间通信。采用Unix通信域套接口有几个好处:Unix通信域套接口通常是TCP套接口速度的两倍;另一个好处是,通过Unix通信域套接口可以实现在进程间传递描述字。所有可用描述字描述的对象,如文件、管道、有名管道及套接口等,在我们以某种方式得到该对象的描述字后,都可以通过基于Unix域的套接口来实现对描述字的传递。接收进程收到的描述字值不一定与发送进程传递的值一致(描述字是特定于进程的),但是特们指向内核文件表中相同的项。

(3)、原始套接口

原始套接口提供一般套接口所不提供的功能:
原始套接口可以读写一些用于控制的控制协议分组,如ICMPv4等,进而可实现一些特殊功能。
原始套接口可以读写特殊的IPv4数据包。内核一般只处理几个特定协议字段的数据包,那么一些需要不同协议字段的数据包就需要通过原始套接口对其进行读写;
通过原始套接口可以构造自己的Ipv4头部,也是比较有意思的一点。

创建原始套接口需要root权限。

(4)、对数据链路层的访问

对数据链路层的访问,使得用户可以侦听本地电缆上的所有分组,而不需要使用任何特殊的硬件设备,在linux下读取数据链路层分组需要创建SOCK_PACKET类型的套接口,并需要有root权限。

(5)、带外数据(out-of-band data)

如果有一些重要信息要立刻通过套接口发送(不经过排队),请查阅与带外数据相关的文献。

(6)、多播

linux内核支持多播,但是在默认状态下,多数linux系统都关闭了对多播的支持。因此,为了实现多播,可能需要重新配置并编译内核。具体请参考[4]及[2]。

结论:linux套接口编程的内容可以说是极大丰富,同时它涉及到许多的网络背景知识,有兴趣的读者可在[2]中找到比较系统而全面的介绍。

参考资料

  1. Understanding the Linux Kernel, 2nd Edition, By Daniel P. Bovet, Marco Cesati , 对各主题阐述得重点突出,脉络清晰。网络部分分析集中在TCP/IP协议栈的数据连路层、网络层以及传输层。
  2. UNIX网络编程第一卷:套接口API和X/Open传输接口API,作者:W.Richard Stevens,译者:杨继张,清华大学出版社。不仅对套接口网络编程有极好的描述,而且极为详尽的阐述了相关的网络背景知识。不论是入门还是深入研究,都是不可多得的好资料。
  3. Linux内核源代码情景分析(下),毛德操、胡希明著,浙江大学出版社,给出了unix域套接口部分的内核代码分析。
  4. GNU/Linux编程指南,入门、应用、精通,第二版,Kurt Wall等著,张辉译

本文转载自这里

信号灯

信号灯与其他进程间通信方式不大相同,它主要提供对进程间共享资源访问控制机制。相当于内存中的标志,进程可以根据它判定是否能够访问某些共享资源,同时,进程也可以修改该标志。除了用于访问控制外,还可用于进程同步。

一、信号灯概述

信号灯与其他进程间通信方式不大相同,它主要提供对进程间共享资源访问控制机制。相当于内存中的标志,进程可以根据它判定是否能够访问某些共享资源,同时,进程也可以修改该标志。除了用于访问控制外,还可用于进程同步。信号灯有以下两种类型:

  1. 二值信号灯:最简单的信号灯形式,信号灯的值只能取0或1,类似于互斥锁。
    注:二值信号灯能够实现互斥锁的功能,但两者的关注内容不同。信号灯强调共享资源,只要共享资源可用,其他进程同样可以修改信号灯的值;互斥锁更强调进程,占用资源的进程使用完资源后,必须由进程本身来解锁。
  2. 计算信号灯:信号灯的值可以取任意非负值(当然受内核本身的约束)。

二、Linux信号灯

linux对信号灯的支持状况与消息队列一样,在red had 8.0发行版本中支持的是系统V的信号灯。因此,本文将主要介绍系统V信号灯及其相应API。在没有声明的情况下,以下讨论中指的都是系统V信号灯。

注意,通常所说的系统V信号灯指的是计数信号灯集。

三、信号灯与内核

  1. 系统V信号灯是随内核持续的,只有在内核重起或者显示删除一个信号灯集时,该信号灯集才会真正被删除。因此系统中记录信号灯的数据结构(struct ipc_ids sem_ids)位于内核中,系统中的所有信号灯都可以在结构sem_ids中找到访问入口。

  2. 下图说明了内核与信号灯是怎样建立起联系的:
    其中:struct ipc_ids sem_ids是内核中记录信号灯的全局数据结构;描述一个具体的信号灯及其相关信息。

其中,struct sem结构如下:

1
2
3
4
struct sem{
int semval; // current value
int sempid // pid of last operation
}

从上图可以看出,全局数据结构struct ipc_ids sem_ids可以访问到struct kern_ipc_perm的第一个成员:struct kern_ipc_perm;而每个struct kern_ipc_perm能够与具体的信号灯对应起来是因为在该结构中,有一个key_t类型成员key,而key则唯一确定一个信号灯集;同时,结构struct kern_ipc_perm的最后一个成员sem_nsems确定了该信号灯在信号灯集中的顺序,这样内核就能够记录每个信号灯的信息了。

1
2
3
4
5
6
7
8
9
10
11
/*系统中的每个信号灯集对应一个sem_array 结构 */
struct sem_array {
struct kern_ipc_perm sem_perm; /* permissions .. see ipc.h */
time_t sem_otime; /* last semop time */
time_t sem_ctime; /* last change time */
struct sem *sem_base; /* ptr to first semaphore in array */
struct sem_queue *sem_pending; /* pending operations to be processed */
struct sem_queue **sem_pending_last; /* last pending operation */
struct sem_undo *undo; /* undo requests on this array */
unsigned long sem_nsems; /* no. of semaphores in array */
};

其中,sem_queue结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/* 系统中每个因为信号灯而睡眠的进程,都对应一个sem_queue结构*/
struct sem_queue {
struct sem_queue * next; /* next entry in the queue */
struct sem_queue ** prev;
/* previous entry in the queue, *(q->prev) == q */
struct task_struct* sleeper; /* this process */
struct sem_undo * undo; /* undo structure */
int pid; /* process id of requesting process */
int status; /* completion status of operation */
struct sem_array * sma; /* semaphore array for operations */
int id; /* internal sem id */
struct sembuf * sops; /* array of pending operations */
int nsops; /* number of operations */
int alter; /* operation will alter semaphore */
};

四、操作信号灯

对消息队列的操作无非有下面三种类型:

  1. 打开或创建信号灯
    与消息队列的创建及打开基本相同,不再详述。

  2. 信号灯值操作
    linux可以增加或减小信号灯的值,相应于对共享资源的释放和占有。具体参见后面的semop系统调用。

  3. 获得或设置信号灯属性:
    系统中的每一个信号灯集都对应一个struct sem_array结构,该结构记录了信号灯集的各种信息,存在于系统空间。为了设置、获得该信号灯集的各种信息及属性,在用户空间有一个重要的联合结构与之对应,即union semun。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
union semun {
int val; /* value for SETVAL */
struct semid_ds *buf; /* buffer for IPC_STAT & IPC_SET */
unsigned short *array; /* array for GETALL & SETALL */
struct seminfo *__buf; /* buffer for IPC_INFO */ //test!!
void *__pad;
};
struct seminfo {
int semmap;
int semmni;
int semmns;
int semmnu;
int semmsl;
int semopm;
int semume;
int semusz;
int semvmx;
int semaem;
};

信号灯API

1、文件名到键值

1
2
3
#include <sys/types.h>
#include <sys/ipc.h>
key_t ftok (char*pathname, char proj);

它返回与路径pathname相对应的一个键值.

2、linux特有的ipc()调用:

1
int ipc(unsigned int call, int first, int second, int third, void \*ptr, long fifth);

参数call取不同值时,对应信号灯的三个系统调用:

  1. 当call为SEMOP时,对应int semop(int semid, struct sembuf *sops, unsigned nsops)调用;
  2. 当call为SEMGET时,对应int semget(key_t key, int nsems, int semflg)调用;
  3. 当call为SEMCTL时,对应int semctl(int semid,int semnum,int cmd,union semun arg)调用;
    这些调用将在后面阐述。

3、系统V信号灯API

系统V消息队列API只有三个,使用时需要包括几个头文件:

1
2
3
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>

1)int semget(key_t key, int nsems, int semflg)

参数key是一个键值,由ftok获得,唯一标识一个信号灯集,用法与msgget()中的key相同;参数nsems指定打开或者新创建的信号灯集中将包含信号灯的数目;semflg参数是一些标志位。参数key和semflg的取值,以及何时打开已有信号灯集或者创建一个新的信号灯集与msgget()中的对应部分相同,不再祥述。
该调用返回与健值key相对应的信号灯集描述字。
调用返回:成功返回信号灯集描述字,否则返回-1。
注:如果key所代表的信号灯已经存在,且semget指定了IPC_CREAT|IPC_EXCL标志,那么即使参数nsems与原来信号灯的数目不等,返回的也是EEXIST错误;如果semget只指定了IPC_CREAT标志,那么参数nsems必须与原来的值一致,在后面程序实例中还要进一步说明。

2)int semop(int semid, struct sembuf *sops, unsigned nsops);

semid是信号灯集ID,sops指向数组的每一个sembuf结构都刻画一个在特定信号灯上的操作。nsops为sops指向数组的大小。
sembuf结构如下:

1
2
3
4
5
struct sembuf {
unsigned short sem_num; /* semaphore index in array */
short sem_op; /* semaphore operation */
short sem_flg; /* operation flags */
};

sem_num对应信号集中的信号灯,0对应第一个信号灯。sem_flg可取IPC_NOWAIT以及SEM_UNDO两个标志。如果设置了SEM_UNDO标志,那么在进程结束时,相应的操作将被取消,这是比较重要的一个标志位。如果设置了该标志位,那么在进程没有释放共享资源就退出时,内核将代为释放。如果为一个信号灯设置了该标志,内核都要分配一个sem_undo结构来记录它,为的是确保以后资源能够安全释放。事实上,如果进程退出了,那么它所占用就释放了,但信号灯值却没有改变,此时,信号灯值反映的已经不是资源占有的实际情况,在这种情况下,问题的解决就靠内核来完成。这有点像僵尸进程,进程虽然退出了,资源也都释放了,但内核进程表中仍然有它的记录,此时就需要父进程调用waitpid来解决问题了。
sem_op的值大于0,等于0以及小于0确定了对sem_num指定的信号灯进行的三种操作。具体请参考linux相应手册页。
这里需要强调的是semop同时操作多个信号灯,在实际应用中,对应多种资源的申请或释放。semop保证操作的原子性,这一点尤为重要。尤其对于多种资源的申请来说,要么一次性获得所有资源,要么放弃申请,要么在不占有任何资源情况下继续等待,这样,一方面避免了资源的浪费;另一方面,避免了进程之间由于申请共享资源造成死锁。
也许从实际含义上更好理解这些操作:信号灯的当前值记录相应资源目前可用数目;sem_op>0对应相应进程要释放sem_op数目的共享资源;sem_op=0可以用于对共享资源是否已用完的测试;sem_op<0相当于进程要申请-sem_op个共享资源。再联想操作的原子性,更不难理解该系统调用何时正常返回,何时睡眠等待。
调用返回:成功返回0,否则返回-1。

3) int semctl(int semid,int semnum,int cmd,union semun arg)

该系统调用实现对信号灯的各种控制操作,参数semid指定信号灯集,参数cmd指定具体的操作类型;参数semnum指定对哪个信号灯操作,只对几个特殊的cmd操作有意义;arg用于设置或返回信号灯信息。
该系统调用详细信息请参见其手册页,这里只给出参数cmd所能指定的操作。

  1. IPC_STAT

    获取信号灯信息,信息由arg.buf返回;

  1. IPC_SET 设置信号灯信息,待设置信息保存在arg.buf中(在manpage中给出了可以设置哪些信息);
    GETALL 返回所有信号灯的值,结果保存在arg.array中,参数sennum被忽略;
    GETNCNT 返回等待semnum所代表信号灯的值增加的进程数,相当于目前有多少进程在等待semnum代表的信号灯所代表的共享资源;
    GETPID 返回最后一个对semnum所代表信号灯执行semop操作的进程ID;
    GETVAL 返回semnum所代表信号灯的值;
    GETZCNT 返回等待semnum所代表信号灯的值变成0的进程数;
    SETALL 通过arg.array更新所有信号灯的值;同时,更新与本信号集相关的semid_ds结构的sem_ctime成员;
    SETVAL 设置semnum所代表信号灯的值为arg.val;

    调用返回:调用失败返回-1,成功返回与cmd相关:

Cmd return value

GETNCNT Semncnt
GETPID Sempid
GETVAL Semval
GETZCNT Semzcnt

五、信号灯的限制

  1. 一次系统调用semop可同时操作的信号灯数目SEMOPM,semop中的参数nsops如果超过了这个数目,将返回E2BIG错误。SEMOPM的大小特定与系统,redhat 8.0为32。

  2. 信号灯的最大数目:SEMVMX,当设置信号灯值超过这个限制时,会返回ERANGE错误。在redhat 8.0中该值为32767。

  3. 系统范围内信号灯集的最大数目SEMMNI以及系统范围内信号灯的最大数目SEMMNS。超过这两个限制将返回ENOSPC错误。redhat 8.0中该值为32000。

  4. 每个信号灯集中的最大信号灯数目SEMMSL,redhat 8.0中为250。 SEMOPM以及SEMVMX是使用semop调用时应该注意的;SEMMNI以及SEMMNS是调用semget时应该注意的。SEMVMX同时也是semctl调用应该注意的。

六、竞争问题

第一个创建信号灯的进程同时也初始化信号灯,这样,系统调用semget包含了两个步骤:创建信号灯;初始化信号灯。由此可能导致一种竞争状态:第一个创建信号灯的进程在初始化信号灯时,第二个进程又调用semget,并且发现信号灯已经存在,此时,第二个进程必须具有判断是否有进程正在对信号灯进行初始化的能力。在参考文献[1]中,给出了绕过这种竞争状态的方法:当semget创建一个新的信号灯时,信号灯结构semid_ds的sem_otime成员初始化后的值为0。因此,第二个进程在成功调用semget后,可再次以IPC_STAT命令调用semctl,等待sem_otime变为非0值,此时可判断该信号灯已经初始化完毕。下图描述了竞争状态产生及解决方法:

实际上,这种解决方法也是基于这样一个假定:第一个创建信号灯的进程必须调用semop,这样sem_otime才能变为非零值。另外,因为第一个进程可能不调用semop,或者semop操作需要很长时间,第二个进程可能无限期等待下去,或者等待很长时间。

七、信号灯应用实例

本实例有两个目的:1、获取各种信号灯信息;2、利用信号灯实现共享资源的申请和释放。并在程序中给出了详细注释。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
#include <linux/sem.h>
#include <stdio.h>
#include <errno.h>
#define SEM_PATH "/unix/my_sem"
#define max_tries 3
int semid;
main()
{
int flag1,flag2,key,i,init_ok,tmperrno;
struct semid_ds sem_info;
struct seminfo sem_info2;
union semun arg; //union semun: 请参考附录2
struct sembuf askfor_res, free_res;
flag1=IPC_CREAT|IPC_EXCL|00666;
flag2=IPC_CREAT|00666;
key=ftok(SEM_PATH,'a');
//error handling for ftok here;
init_ok=0;
semid=semget(key,1,flag1);
//create a semaphore set that only includes one semphore.
if(semid<0)
{
tmperrno=errno;
perror("semget");
if(tmperrno==EEXIST)
//errno is undefined after a successful library call( including perror call)
//so it is saved in tmperrno.
{
semid=semget(key,1,flag2);
//flag2 只包含了IPC_CREAT标志, 参数nsems(这里为1)必须与原来的信号灯数目一致
arg.buf=&sem_info;
for(i=0; i<max_tries; i++)
{
if(semctl(semid, 0, IPC_STAT, arg)==-1)
{ perror("semctl error"); i=max_tries;}
else
{
if(arg.buf->sem_otime!=0){ i=max_tries; init_ok=1;}
else sleep(1);
}
}
if(!init_ok)
// do some initializing, here we assume that the first process that creates the sem
// will finish initialize the sem and run semop in max_tries*1 seconds. else it will
// not run semop any more.
{
arg.val=1;
if(semctl(semid,0,SETVAL,arg)==-1) perror("semctl setval error");
}
}
else
{perror("semget error, process exit"); exit(); }
}
else //semid>=0; do some initializing
{
arg.val=1;
if(semctl(semid,0,SETVAL,arg)==-1)
perror("semctl setval error");
}
//get some information about the semaphore and the limit of semaphore in redhat8.0
arg.buf=&sem_info;
if(semctl(semid, 0, IPC_STAT, arg)==-1)
perror("semctl IPC STAT");
printf("owner's uid is %d\n", arg.buf->sem_perm.uid);
printf("owner's gid is %d\n", arg.buf->sem_perm.gid);
printf("creater's uid is %d\n", arg.buf->sem_perm.cuid);
printf("creater's gid is %d\n", arg.buf->sem_perm.cgid);
arg.__buf=&sem_info2;
if(semctl(semid,0,IPC_INFO,arg)==-1)
perror("semctl IPC_INFO");
printf("the number of entries in semaphore map is %d \n", arg.__buf->semmap);
printf("max number of semaphore identifiers is %d \n", arg.__buf->semmni);
printf("mas number of semaphores in system is %d \n", arg.__buf->semmns);
printf("the number of undo structures system wide is %d \n", arg.__buf->semmnu);
printf("max number of semaphores per semid is %d \n", arg.__buf->semmsl);
printf("max number of ops per semop call is %d \n", arg.__buf->semopm);
printf("max number of undo entries per process is %d \n", arg.__buf->semume);
printf("the sizeof of struct sem_undo is %d \n", arg.__buf->semusz);
printf("the maximum semaphore value is %d \n", arg.__buf->semvmx);

//now ask for available resource:
askfor_res.sem_num=0;
askfor_res.sem_op=-1;
askfor_res.sem_flg=SEM_UNDO;

if(semop(semid,&askfor_res,1)==-1)//ask for resource
perror("semop error");

sleep(3);
//do some handling on the sharing resource here, just sleep on it 3 seconds
printf("now free the resource\n");

//now free resource
free_res.sem_num=0;
free_res.sem_op=1;
free_res.sem_flg=SEM_UNDO;
if(semop(semid,&free_res,1)==-1)//free the resource.
if(errno==EIDRM)
printf("the semaphore set was removed\n");
//you can comment out the codes below to compile a different version:
if(semctl(semid, 0, IPC_RMID)==-1)
perror("semctl IPC_RMID");
else printf("remove sem ok\n");
}

注:读者可以尝试一下注释掉初始化步骤,进程在运行时会出现何种情况(进程在申请资源时会睡眠),同时可以像程序结尾给出的注释那样,把该程序编译成两个不同版本。下面是本程序的运行结果(操作系统redhat8.0):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
owner's uid is 0
owner's gid is 0

creater's uid is 0
creater's gid is 0

the number of entries in semaphore map is 32000
max number of semaphore identifiers is 128
mas number of semaphores in system is 32000
the number of undo structures system wide is 32000
max number of semaphores per semid is 250
max number of ops per semop call is 32
max number of undo entries per process is 32
the sizeof of struct sem_undo is 20
the maximum semaphore value is 32767
now free the resource
remove sem ok

Summary:信号灯与其它进程间通信方式有所不同,它主要用于进程间同步。通常所说的系统V信号灯实际上是一个信号灯的集合,可用于多种共享资源的进程间同步。每个信号灯都有一个值,可以用来表示当前该信号灯代表的共享资源可用(available)数量,如果一个进程要申请共享资源,那么就从信号灯值中减去要申请的数目,如果当前没有足够的可用资源,进程可以睡眠等待,也可以立即返回。当进程要申请多种共享资源时,linux可以保证操作的原子性,即要么申请到所有的共享资源,要么放弃所有资源,这样能够保证多个进程不会造成互锁。Linux对信号灯有各种各样的限制,程序中给出了输出结果。另外,如果读者想对信号灯作进一步的理解,建议阅读sem.h源代码,该文件不长,但给出了信号灯相关的重要数据结构。

本文转载自这里

消息队列

消息队列(也叫做报文队列)能够克服早期unix通信机制的一些缺点。作为早期unix通信机制之一的信号能够传送的信息量有限,后来虽然POSIX 1003.1b在信号的实时性方面作了拓广,使得信号在传递信息量方面有了相当程度的改进,但是信号这种通信方式更像”即时”的通信方式,它要求接受信号的进程在某个时间范围内对信号做出反应,因此该信号最多在接受信号进程的生命周期内才有意义,信号所传递的信息是接近于随进程持续的概念(process-persistent),见 附录 1;管道及有名管道及有名管道则是典型的随进程持续IPC,并且,只能传送无格式的字节流无疑会给应用程序开发带来不便,另外,它的缓冲区大小也受到限制。

消息队列就是一个消息的链表。可以把消息看作一个记录,具有特定的格式以及特定的优先级。对消息队列有写权限的进程可以向中按照一定的规则添加新消息;对消息队列有读权限的进程则可以从消息队列中读走消息。消息队列是随内核持续的(参见 附录 1)。

目前主要有两种类型的消息队列:POSIX消息队列以及系统V消息队列,系统V消息队列目前被大量使用。考虑到程序的可移植性,新开发的应用程序应尽量使用POSIX消息队列。

在本系列专题的序(深刻理解Linux进程间通信(IPC))中,提到对于消息队列、信号灯、以及共享内存区来说,有两个实现版本:POSIX的以及系统V的。Linux内核(内核2.4.18)支持POSIX信号灯、POSIX共享内存区以及POSIX消息队列,但对于主流Linux发行版本之一redhad8.0(内核2.4.18),还没有提供对POSIX进程间通信API的支持,不过应该只是时间上的事。

因此,本文将主要介绍系统V消息队列及其相应API。 在没有声明的情况下,以下讨论中指的都是系统V消息队列。

消息队列基本概念

1、系统V消息队列是随内核持续的,只有在内核重起或者显示删除一个消息队列时,该消息队列才会真正被删除。因此系统中记录消息队列的数据结构(struct ipc_ids msg_ids)位于内核中,系统中的所有消息队列都可以在结构msg_ids中找到访问入口。
2、消息队列就是一个消息的链表。每个消息队列都有一个队列头,用结构struct msg_queue来描述(参见 附录 2)。队列头中包含了该消息队列的大量信息,包括消息队列键值、用户ID、组ID、消息队列中消息数目等等,甚至记录了最近对消息队列读写进程的ID。读者可以访问这些信息,也可以设置其中的某些信息。
3、下图说明了内核与消息队列是怎样建立起联系的:
其中:struct ipc_ids msg_ids是内核中记录消息队列的全局数据结构;struct msg_queue是每个消息队列的队列头。

从上图可以看出,全局数据结构 struct ipc_ids msg_ids 可以访问到每个消息队列头的第一个成员:struct kern_ipc_perm;而每个struct kern_ipc_perm能够与具体的消息队列对应起来是因为在该结构中,有一个key_t类型成员key,而key则唯一确定一个消息队列。kern_ipc_perm结构如下:

1
2
3
4
5
6
7
8
9
struct kern_ipc_perm{   //内核中记录消息队列的全局数据结构msg_ids能够访问到该结构;
key_t key; //该键值则唯一对应一个消息队列
uid_t uid;
gid_t gid;
uid_t cuid;
gid_t cgid;
mode_t mode;
unsigned long seq;
}

操作消息队列

对消息队列的操作无非有下面三种类型:

1、 打开或创建消息队列
消息队列的内核持续性要求每个消息队列都在系统范围内对应唯一的键值,所以,要获得一个消息队列的描述字,只需提供该消息队列的键值即可;

注:消息队列描述字是由在系统范围内唯一的键值生成的,而键值可以看作对应系统内的一条路经。

2、 读写操作
消息读写操作非常简单,对开发人员来说,每个消息都类似如下的数据结构:

1
2
3
4
struct msgbuf{
long mtype;
char mtext[1];
};

mtype成员代表消息类型,从消息队列中读取消息的一个重要依据就是消息的类型;mtext是消息内容,当然长度不一定为1。因此,对于发送消息来说,首先预置一个msgbuf缓冲区并写入消息类型和内容,调用相应的发送函数即可;对读取消息来说,首先分配这样一个msgbuf缓冲区,然后把消息读入该缓冲区即可。

3、 获得或设置消息队列属性:
消息队列的信息基本上都保存在消息队列头中,因此,可以分配一个类似于消息队列头的结构(struct msqid_ds,见 附录 2),来返回消息队列的属性;同样可以设置该数据结构。

消息队列API

1、文件名到键值

1
2
3
#include <sys/types.h>
#include <sys/ipc.h>
key_t ftok (char*pathname, char proj);

它返回与路径pathname相对应的一个键值。该函数不直接对消息队列操作,但在调用ipc(MSGGET,…)或msgget()来获得消息队列描述字前,往往要调用该函数。典型的调用代码是:

1
2
3
key=ftok(path_ptr, 'a');
ipc_id=ipc(MSGGET, (int)key, flags,0,NULL,0);

2、linux为操作系统V进程间通信的三种方式(消息队列、信号灯、共享内存区)提供了一个统一的用户界面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int ipc(unsigned int call, int first, int second, int third, void * ptr, long fifth);
第一个参数指明对IPC对象的操作方式,对消息队列而言共有四种操作:MSGSND、MSGRCV、MSGGET以及MSGCTL,分别代表向消息队列发送消息、从消息队列读取消息、打开或创建消息队列、控制消息队列;first参数代表唯一的IPC对象;下面将介绍四种操作.

int ipc( MSGGET, intfirst, intsecond, intthird, void*ptr, longfifth);
与该操作对应的系统V调用为:int msgget( (key_t)first,second)。

int ipc( MSGCTL, intfirst, intsecond, intthird, void*ptr, longfifth)
与该操作对应的系统V调用为:int msgctl( first,second, (struct msqid_ds*) ptr)。

int ipc( MSGSND, intfirst, intsecond, intthird, void*ptr, longfifth);
与该操作对应的系统V调用为:int msgsnd( first, (struct msgbuf*)ptr, second, third)。

int ipc( MSGRCV, intfirst, intsecond, intthird, void*ptr, longfifth);
与该操作对应的系统V调用为:int msgrcv( first,(struct msgbuf*)ptr, second, fifth,third),

注:本人不主张采用系统调用ipc(),而更倾向于采用系统V或者POSIX进程间通信API。原因如下:

  • 虽然该系统调用提供了统一的用户界面,但正是由于这个特性,它的参数几乎不能给出特定的实际意义(如以first、second来命名参数),在一定程度上造成开发不便。

  • 正如ipc手册所说的:ipc()是linux所特有的,编写程序时应注意程序的移植性问题;

  • 该系统调用的实现不过是把系统V IPC函数进行了封装,没有任何效率上的优势;

  • 系统V在IPC方面的API数量不多,形式也较简洁。
  • 系统V消息队列API

    系统V消息队列API共有四个,使用时需要包括几个头文件:

    1
    2
    3
    #include <sys/types.h>
    #include <sys/ipc.h>
    #include <sys/msg.h>

    int msgget(key_t key, int msgflg)

    参数key是一个键值,由ftok获得;msgflg参数是一些标志位。该调用返回与健值key相对应的消息队列描述字。

    在以下两种情况下,该调用将创建一个新的消息队列:

  • 如果没有消息队列与健值key相对应,并且msgflg中包含了IPC_CREAT标志位;

  • key参数为IPC_PRIVATE;
  • 参数msgflg可以为以下:IPC_CREAT、IPC_EXCL、IPC_NOWAIT或三者的或结果。

    调用返回:成功返回消息队列描述字,否则返回-1。

    注:参数key设置成常数IPC_PRIVATE并不意味着其他进程不能访问该消息队列,只意味着即将创建新的消息队列。

    int msgrcv(int msqid, struct msgbuf *msgp, int msgsz, long msgtyp, int msgflg);

    该系统调用从msgid代表的消息队列中读取一个消息,并把消息存储在msgp指向的msgbuf结构中。

    msqid为消息队列描述字;消息返回后存储在msgp指向的地址,msgsz指定msgbuf的mtext成员的长度(即消息内容的长度),msgtyp为请求读取的消息类型;读消息标志msgflg可以为以下几个常值的或:

  • IPC_NOWAIT 如果没有满足条件的消息,调用立即返回,此时,errno=ENOMSG

  • IPC_EXCEPT 与msgtyp>0配合使用,返回队列中第一个类型不为msgtyp的消息

  • IPC_NOERROR 如果队列中满足条件的消息内容大于所请求的msgsz字节,则把该消息截断,截断部分将丢失。
  • msgrcv手册中详细给出了消息类型取不同值时(>0; <0; =0),调用将返回消息队列中的哪个消息。

    msgrcv()解除阻塞的条件有三个:

    1.消息队列中有了满足条件的消息;
    2.msqid代表的消息队列被删除;
    3.调用msgrcv()的进程被信号中断;

    调用返回:成功返回读出消息的实际字节数,否则返回-1。

    int msgsnd(int msqid, struct msgbuf *msgp, int msgsz, int msgflg);

    向msgid代表的消息队列发送一个消息,即将发送的消息存储在msgp指向的msgbuf结构中,消息的大小由msgze指定。

    对发送消息来说,有意义的msgflg标志为IPC_NOWAIT,指明在消息队列没有足够空间容纳要发送的消息时,msgsnd是否等待。造成msgsnd()等待的条件有两种:

    当前消息的大小与当前消息队列中的字节数之和超过了消息队列的总容量;
    当前消息队列的消息数(单位”个”)不小于消息队列的总容量(单位”字节数”),此时,虽然消息队列中的消息数目很多,但基本上都只有一个字节。

    msgsnd()解除阻塞的条件有三个:
    1.不满足上述两个条件,即消息队列中有容纳该消息的空间;
    2.msqid代表的消息队列被删除;
    3.调用msgsnd()的进程被信号中断;

    调用返回:成功返回0,否则返回-1。

    int msgctl(int msqid, int cmd, struct msqid_ds *buf);

    该系统调用对由msqid标识的消息队列执行cmd操作,共有三种cmd操作:IPC_STAT、IPC_SET 、IPC_RMID。

    1. IPC_STAT:该命令用来获取消息队列信息,返回的信息存贮在buf指向的msqid结构中;
    2. IPC_SET:该命令用来设置消息队列的属性,要设置的属性存储在buf指向的msqid结构中;可设置属性包括:msg_perm.uid、msg_perm.gid、msg_perm.mode以及msg_qbytes,同时,也影响msg_ctime成员。
    3. IPC_RMID:删除msqid标识的消息队列;

    调用返回:成功返回0,否则返回-1。

    消息队列的限制

    每个消息队列的容量(所能容纳的字节数)都有限制,该值因系统不同而不同。在后面的应用实例中,输出了redhat 8.0的限制,结果参见 附录 3。

    另一个限制是每个消息队列所能容纳的最大消息数:在redhad 8.0中,该限制是受消息队列容量制约的:消息个数要小于消息队列的容量(字节数)。

    注:上述两个限制是针对每个消息队列而言的,系统对消息队列的限制还有系统范围内的最大消息队列个数,以及整个系统范围内的最大消息数。一般来说,实际开发过程中不会超过这个限制。

    消息队列应用实例

    消息队列应用相对较简单,下面实例基本上覆盖了对消息队列的所有操作,同时,程序输出结果有助于加深对前面所讲的某些规则及消息队列限制的理解。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    #include <sys/types.h>
    #include <sys/msg.h>
    #include <unistd.h>
    void msg_stat(int,struct msqid_ds );
    main()
    {
    int gflags,sflags,rflags;
    key_t key;
    int msgid;
    int reval;
    struct msgsbuf{
    int mtype;
    char mtext[1];
    }msg_sbuf;
    struct msgmbuf
    {
    int mtype;
    char mtext[10];
    }msg_rbuf;
    struct msqid_ds msg_ginfo,msg_sinfo;
    char* msgpath="/unix/msgqueue";
    key=ftok(msgpath,'a');
    gflags=IPC_CREAT|IPC_EXCL;
    msgid=msgget(key,gflags|00666);
    if(msgid==-1)
    {
    printf("msg create error\n");
    return;
    }
    //创建一个消息队列后,输出消息队列缺省属性
    msg_stat(msgid,msg_ginfo);
    sflags=IPC_NOWAIT;
    msg_sbuf.mtype=10;
    msg_sbuf.mtext[0]='a';
    reval=msgsnd(msgid,&msg_sbuf,sizeof(msg_sbuf.mtext),sflags);
    if(reval==-1)
    {
    printf("message send error\n");
    }
    //发送一个消息后,输出消息队列属性
    msg_stat(msgid,msg_ginfo);
    rflags=IPC_NOWAIT|MSG_NOERROR;
    reval=msgrcv(msgid,&msg_rbuf,4,10,rflags);
    if(reval==-1)
    printf("read msg error\n");
    else
    printf("read from msg queue %d bytes\n",reval);
    //从消息队列中读出消息后,输出消息队列属性
    msg_stat(msgid,msg_ginfo);
    msg_sinfo.msg_perm.uid=8;//just a try
    msg_sinfo.msg_perm.gid=8;//
    msg_sinfo.msg_qbytes=16388;
    //此处验证超级用户可以更改消息队列的缺省msg_qbytes
    //注意这里设置的值大于缺省值
    reval=msgctl(msgid,IPC_SET,&msg_sinfo);
    if(reval==-1)
    {
    printf("msg set info error\n");
    return;
    }
    msg_stat(msgid,msg_ginfo);
    //验证设置消息队列属性
    reval=msgctl(msgid,IPC_RMID,NULL);//删除消息队列
    if(reval==-1)
    {
    printf("unlink msg queue error\n");
    return;
    }
    }
    void msg_stat(int msgid,struct msqid_ds msg_info)
    {

    int reval;
    sleep(1);//只是为了后面输出时间的方便
    reval=msgctl(msgid,IPC_STAT,&msg_info);
    if(reval==-1)
    {
    printf("get msg info error\n");
    return;
    }
    printf("\n");
    printf("current number of bytes on queue is %d\n",msg_info.msg_cbytes);
    printf("number of messages in queue is %d\n",msg_info.msg_qnum);
    printf("max number of bytes on queue is %d\n",msg_info.msg_qbytes);
    //每个消息队列的容量(字节数)都有限制MSGMNB,值的大小因系统而异。在创建新的消息队列时,//msg_qbytes的缺省值就是MSGMNB
    printf("pid of last msgsnd is %d\n",msg_info.msg_lspid);
    printf("pid of last msgrcv is %d\n",msg_info.msg_lrpid);
    printf("last msgsnd time is %s", ctime(&(msg_info.msg_stime)));
    printf("last msgrcv time is %s", ctime(&(msg_info.msg_rtime)));
    printf("last change time is %s", ctime(&(msg_info.msg_ctime)));
    printf("msg uid is %d\n",msg_info.msg_perm.uid);
    printf("msg gid is %d\n",msg_info.msg_perm.gid);
    }

    程序输出结果见 附录 3。

    小结:

    消息队列与管道以及有名管道相比,具有更大的灵活性,首先,它提供有格式字节流,有利于减少开发人员的工作量;其次,消息具有类型,在实际应用中,可作为优先级使用。这两点是管道以及有名管道所不能比的。同样,消息队列可以在几个进程间复用,而不管这几个进程是否具有亲缘关系,这一点与有名管道很相似;但消息队列是随内核持续的,与有名管道(随进程持续)相比,生命力更强,应用空间更大。

    附录 1: 在参考文献[1]中,给出了IPC随进程持续、随内核持续以及随文件系统持续的定义:

    1. 随进程持续:IPC一直存在到打开IPC对象的最后一个进程关闭该对象为止。如管道和有名管道;
    2. 随内核持续:IPC一直持续到内核重新自举或者显示删除该对象为止。如消息队列、信号灯以及共享内存等;
    3. 随文件系统持续:IPC一直持续到显示删除该对象为止。

    附录 2:

    结构msg_queue用来描述消息队列头,存在于系统空间:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    struct msg_queue {
    struct kern_ipc_perm q_perm;
    time_t q_stime; /* last msgsnd time */
    time_t q_rtime; /* last msgrcv time */
    time_t q_ctime; /* last change time */
    unsigned long q_cbytes; /* current number of bytes on queue */
    unsigned long q_qnum; /* number of messages in queue */
    unsigned long q_qbytes; /* max number of bytes on queue */
    pid_t q_lspid; /* pid of last msgsnd */
    pid_t q_lrpid; /* last receive pid */
    struct list_head q_messages;
    struct list_head q_receivers;
    struct list_head q_senders;
    };

    结构msqid_ds用来设置或返回消息队列的信息,存在于用户空间;

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    struct msqid_ds {
    struct ipc_perm msg_perm;
    struct msg *msg_first; /* first message on queue,unused */
    struct msg *msg_last; /* last message in queue,unused */
    __kernel_time_t msg_stime; /* last msgsnd time */
    __kernel_time_t msg_rtime; /* last msgrcv time */
    __kernel_time_t msg_ctime; /* last change time */
    unsigned long msg_lcbytes; /* Reuse junk fields for 32 bit */
    unsigned long msg_lqbytes; /* ditto */
    unsigned short msg_cbytes; /* current number of bytes on queue */
    unsigned short msg_qnum; /* number of messages in queue */
    unsigned short msg_qbytes; /* max number of bytes on queue */
    __kernel_ipc_pid_t msg_lspid; /* pid of last msgsnd */
    __kernel_ipc_pid_t msg_lrpid; /* last receive pid */
    };

    可以看出上述两个结构很相似。

    附录 3: 消息队列实例输出结果:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    current number of bytes on queue is 0
    number of messages in queue is 0
    max number of bytes on queue is 16384
    pid of last msgsnd is 0
    pid of last msgrcv is 0
    last msgsnd time is Thu Jan 1 08:00:00 1970
    last msgrcv time is Thu Jan 1 08:00:00 1970
    last change time is Sun Dec 29 18:28:20 2002
    msg uid is 0
    msg gid is 0
    //上面刚刚创建一个新消息队列时的输出
    current number of bytes on queue is 1
    number of messages in queue is 1
    max number of bytes on queue is 16384
    pid of last msgsnd is 2510
    pid of last msgrcv is 0
    last msgsnd time is Sun Dec 29 18:28:21 2002
    last msgrcv time is Thu Jan 1 08:00:00 1970
    last change time is Sun Dec 29 18:28:20 2002
    msg uid is 0
    msg gid is 0
    read from msg queue 1 bytes
    //实际读出的字节数
    current number of bytes on queue is 0
    number of messages in queue is 0
    max number of bytes on queue is 16384 //每个消息队列最大容量(字节数)
    pid of last msgsnd is 2510
    pid of last msgrcv is 2510
    last msgsnd time is Sun Dec 29 18:28:21 2002
    last msgrcv time is Sun Dec 29 18:28:22 2002
    last change time is Sun Dec 29 18:28:20 2002
    msg uid is 0
    msg gid is 0
    current number of bytes on queue is 0
    number of messages in queue is 0
    max number of bytes on queue is 16388 //可看出超级用户可修改消息队列最大容量
    pid of last msgsnd is 2510
    pid of last msgrcv is 2510 //对操作消息队列进程的跟踪
    last msgsnd time is Sun Dec 29 18:28:21 2002
    last msgrcv time is Sun Dec 29 18:28:22 2002
    last change time is Sun Dec 29 18:28:23 2002 //msgctl()调用对msg_ctime有影响
    msg uid is 8
    msg gid is 8

    参考文献

    1. UNIX网络编程第二卷:进程间通信,作者:W.Richard Stevens,译者:杨继张,清华大学出版社。对POSIX以及系统V消息队列都有阐述,对Linux环境下的程序开发有极大的启发意义

    本文转载自这里

    共享内存

    共享内存可以说是最有用的进程间通信方式,也是最快的IPC形式。两个不同进程A、B共享内存的意思是,同一块物理内存被映射到进程A、B各自的进程地址空间。进程A可以即时看到进程B对共享内存中数据的更新,反之亦然。由于多个进程共享同一块内存区域,必然需要某种同步机制,互斥锁和信号量都可以。

    采用共享内存通信的一个显而易见的好处是效率高,因为进程可以直接读写内存,而不需要任何数据的拷贝。对于像管道和消息队列等通信方式,则需要在内核和用户空间进行四次的数据拷贝,而共享内存则只拷贝两次数据[1]:一次从输入文件到共享内存区,另一次从共享内存区到输出文件。实际上,进程之间在共享内存时,并不总是读写少量数据后就解除映射,有新的通信时,再重新建立共享内存区域。而是保持共享区域,直到通信完毕为止,这样,数据内容一直保存在共享内存中,并没有写回文件。共享内存中的内容往往是在解除映射时才写回文件的。因此,采用共享内存的通信方式效率是非常高的。

    Linux的2.2.x内核支持多种共享内存方式,如mmap()系统调用,Posix共享内存,以及系统V共享内存。linux发行版本如Redhat 8.0支持mmap()系统调用及系统V共享内存,但还没实现Posix共享内存,本文将主要介绍mmap()系统调用及系统V共享内存API的原理及应用。

    一、内核怎样保证各个进程寻址到同一个共享内存区域的内存页面

    1、page cache及swap cache中页面的区分:一个被访问文件的物理页面都驻留在page cache或swap cache中,一个页面的所有信息由struct page来描述。struct page中有一个域为指针mapping ,它指向一个struct address_space类型结构。page cache或swap cache中的所有页面就是根据address_space结构以及一个偏移量来区分的。

    2、文件与address_space结构的对应:一个具体的文件在打开后,内核会在内存中为之建立一个struct inode结构,其中的i_mapping域指向一个address_space结构。这样,一个文件就对应一个address_space结构,一个address_space与一个偏移量能够确定一个page cache 或swap cache中的一个页面。因此,当要寻址某个数据时,很容易根据给定的文件及数据在文件内的偏移量而找到相应的页面。

    3、进程调用mmap()时,只是在进程空间内新增了一块相应大小的缓冲区,并设置了相应的访问标识,但并没有建立进程空间到物理页面的映射。因此,第一次访问该空间时,会引发一个缺页异常。

    4、对于共享内存映射情况,缺页异常处理程序首先在swap cache中寻找目标页(符合address_space以及偏移量的物理页),如果找到,则直接返回地址;如果没有找到,则判断该页是否在交换区(swap area),如果在,则执行一个换入操作;如果上述两种情况都不满足,处理程序将分配新的物理页面,并把它插入到page cache中。进程最终将更新进程页表。
    注:对于映射普通文件情况(非共享映射),缺页异常处理程序首先会在page cache中根据address_space以及数据偏移量寻找相应的页面。如果没有找到,则说明文件数据还没有读入内存,处理程序会从磁盘读入相应的页面,并返回相应地址,同时,进程页表也会更新。

    5、所有进程在映射同一个共享内存区域时,情况都一样,在建立线性地址与物理地址之间的映射之后,不论进程各自的返回地址如何,实际访问的必然是同一个共享内存区域对应的物理页面。
    注:一个共享内存区域可以看作是特殊文件系统shm中的一个文件,shm的安装点在交换区上。

    上面涉及到了一些数据结构,围绕数据结构理解问题会容易一些。

    二、mmap()及其相关系统调用

    mmap()系统调用使得进程之间通过映射同一个普通文件实现共享内存。普通文件被映射到进程地址空间后,进程可以向访问普通内存一样对文件进行访问,不必再调用read(),write()等操作。

    注:实际上,mmap()系统调用并不是完全为了用于共享内存而设计的。它本身提供了不同于一般对普通文件的访问方式,进程可以像读写内存一样对普通文件的操作。而Posix或系统V的共享内存IPC则纯粹用于共享目的,当然mmap()实现共享内存也是其主要应用之一。

    mmap()系统调用形式如下:
    1
    void* mmap ( void * addr , size_t len , int prot , int flags , int fd , off_t offset )

    参数fd为即将映射到进程空间的文件描述字,一般由open()返回,同时,fd可以指定为-1,此时须指定flags参数中的MAP_ANON,表明进行的是匿名映射(不涉及具体的文件名,避免了文件的创建及打开,很显然只能用于具有亲缘关系的进程间通信)。len是映射到调用进程地址空间的字节数,它从被映射文件开头offset个字节开始算起。prot 参数指定共享内存的访问权限。可取如下几个值的或:PROT_READ(可读) , PROT_WRITE (可写), PROT_EXEC (可执行), PROT_NONE(不可访问)。flags由以下几个常值指定:MAP_SHARED , MAP_PRIVATE , MAP_FIXED,其中,MAP_SHARED , MAP_PRIVATE必选其一,而MAP_FIXED则不推荐使用。offset参数一般设为0,表示从文件头开始映射。参数addr指定文件应被映射到进程空间的起始地址,一般被指定一个空指针,此时选择起始地址的任务留给内核来完成。函数的返回值为最后文件映射到进程空间的地址,进程可直接操作起始地址为该值的有效地址。这里不再详细介绍mmap()的参数,读者可参考mmap()手册页获得进一步的信息。

    系统调用mmap()用于共享内存的两种方式:

    (1)使用普通文件提供的内存映射:适用于任何进程之间; 此时,需要打开或创建一个文件,然后再调用mmap();典型调用代码如下:

    1
    2
    3
    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()返回的地址,却由父子进程共同维护。
    对于具有亲缘关系的进程实现共享内存最好的方式应该是采用匿名内存映射的方式。此时,不必指定具体的文件,只要设置相应的标志即可,参见范例2。

    系统调用munmap()
    1
    int munmap( void * addr, size_t len )

    该调用在进程地址空间中解除一个映射关系,addr是调用mmap()时返回的地址,len是映射区的大小。当映射关系解除后,对原来映射地址的访问将导致段错误发生。

    系统调用msync()
    1
    int msync ( void * addr , size_t len, int flags)

    一般说来,进程在映射空间的对共享内容的改变并不直接写回到磁盘文件中,往往在调用munmap()后才执行该操作。可以通过调用msync()实现磁盘上文件内容与共享内存区的内容一致。

    三、mmap()范例

    下面将给出使用mmap()的两个范例:范例1给出两个进程通过映射普通文件实现共享内存通信;范例2给出父子进程通过匿名映射实现共享内存。系统调用mmap()有许多有趣的地方,下面是通过mmap()映射普通文件实现进程间的通信的范例,我们通过该范例来说明mmap()实现共享内存的特点及注意事项。

    范例1:两个进程通过映射普通文件实现共享内存通信

    范例1包含两个子程序:map_normalfile1.c及map_normalfile2.c。编译两个程序,可执行文件分别为map_normalfile1及map_normalfile2。两个程序通过命令行参数指定同一个文件来实现共享内存方式的进程间通信。map_normalfile2试图打开命令行参数指定的一个普通文件,把该文件映射到进程的地址空间,并对映射后的地址空间进行写操作。map_normalfile1把命令行参数指定的文件映射到进程地址空间,然后对映射后的地址空间执行读操作。这样,两个进程通过命令行参数指定同一个文件来实现共享内存方式的进程间通信。

    下面是两个程序代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    /*-------------map_normalfile1.c-----------*/
    #include <sys/mman.h>
    #include <sys/types.h>
    #include <fcntl.h>
    #include <unistd.h>
    typedef struct{
    char name[4];
    int age;
    }people;
    main(int argc, char** argv) // map a normal file as shared mem:
    {
    int fd,i;
    people *p_map;
    char temp;

    fd=open(argv[1],O_CREAT|O_RDWR|O_TRUNC,00777);
    lseek(fd,sizeof(people)*5-1,SEEK_SET);
    write(fd,"",1);

    p_map = (people*) mmap( NULL,sizeof(people)*10,PROT_READ|PROT_WRITE,
    MAP_SHARED,fd,0 );
    close( fd );
    temp = 'a';
    for(i=0; i<10; i++)
    {
    temp += 1;
    memcpy( ( *(p_map+i) ).name, &temp,2 );
    ( *(p_map+i) ).age = 20+i;
    }
    printf(" initialize over \n ");
    sleep(10);
    munmap( p_map, sizeof(people)*10 );
    printf( "umap ok \n" );
    }
    /*-------------map_normalfile2.c-----------*/
    #include <sys/mman.h>
    #include <sys/types.h>
    #include <fcntl.h>
    #include <unistd.h>
    typedef struct{
    char name[4];
    int age;
    }people;
    main(int argc, char** argv) // map a normal file as shared mem:
    {
    int fd,i;
    people *p_map;
    fd=open( argv[1],O_CREAT|O_RDWR,00777 );
    p_map = (people*)mmap(NULL,sizeof(people)*10,PROT_READ|PROT_WRITE,
    MAP_SHARED,fd,0);
    for(i = 0;i<10;i++)
    {
    printf( "name: %s age %d;\n",(*(p_map+i)).name, (*(p_map+i)).age );
    }
    munmap( p_map,sizeof(people)*10 );
    }

    map_normalfile1.c首先定义了一个people数据结构,(在这里采用数据结构的方式是因为,共享内存区的数据往往是有固定格式的,这由通信的各个进程决定,采用结构的方式有普遍代表性)。map_normfile1首先打开或创建一个文件,并把文件的长度设置为5个people结构大小。然后从mmap()的返回地址开始,设置了10个people结构。然后,进程睡眠10秒钟,等待其他进程映射同一个文件,最后解除映射。

    map_normfile2.c只是简单的映射一个文件,并以people数据结构的格式从mmap()返回的地址处读取10个people结构,并输出读取的值,然后解除映射。

    分别把两个程序编译成可执行文件map_normalfile1和map_normalfile2后,在一个终端上先运行./map_normalfile2 /tmp/test_shm,程序输出结果如下:

    1
    2
    initialize over
    umap ok

    在map_normalfile1输出initialize over 之后,输出umap ok之前,在另一个终端上运行map_normalfile2 /tmp/test_shm,将会产生如下输出(为了节省空间,输出结果为稍作整理后的结果):

    1
    2
    name: b	age 20;	name: c	age 21;	name: d	age 22;	name: e	age 23;	name: f	age 24;
    name: g age 25; name: h age 26; name: I age 27; name: j age 28; name: k age 29;

    在map_normalfile1 输出umap ok后,运行map_normalfile2则输出如下结果:

    1
    2
    name: b	age 20;	name: c	age 21;	name: d	age 22;	name: e	age 23;	name: f	age 24;
    name: age 0; name: age 0; name: age 0; name: age 0; name: age 0;

    从程序的运行结果中可以得出的结论

    1、最终被映射文件的内容的长度不会超过文件本身的初始大小,即映射不能改变文件的大小;

    2、可以用于进程通信的有效地址空间大小大体上受限于被映射文件的大小,但不完全受限于文件大小。打开文件被截短为5个people结构大小,而在map_normalfile1中初始化了10个people数据结构,在恰当时候(map_normalfile1输出initialize over 之后,输出umap ok之前)调用map_normalfile2会发现map_normalfile2将输出全部10个people结构的值,后面将给出详细讨论。
    注:在linux中,内存的保护是以页为基本单位的,即使被映射文件只有一个字节大小,内核也会为映射分配一个页面大小的内存。当被映射文件小于一个页面大小时,进程可以对从mmap()返回地址开始的一个页面大小进行访问,而不会出错;但是,如果对一个页面以外的地址空间进行访问,则导致错误发生,后面将进一步描述。因此,可用于进程间通信的有效地址空间大小不会超过文件大小及一个页面大小的和。

    3、文件一旦被映射后,调用mmap()的进程对返回地址的访问是对某一内存区域的访问,暂时脱离了磁盘上文件的影响。所有对mmap()返回地址空间的操作只在内存中有意义,只有在调用了munmap()后或者msync()时,才把内存中的相应内容写回磁盘文件,所写内容仍然不能超过文件的大小。

    范例2:父子进程通过匿名映射实现共享内存

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    #include <sys/mman.h>
    #include <sys/types.h>
    #include <fcntl.h>
    #include <unistd.h>
    typedef struct{
    char name[4];
    int age;
    }people;
    main(int argc, char** argv)
    {
    int i;
    people *p_map;
    char temp;
    p_map=(people*)mmap(NULL,sizeof(people)*10,PROT_READ|PROT_WRITE,
    MAP_SHARED|MAP_ANONYMOUS,-1,0);
    if(fork() == 0)
    {
    sleep(2);
    for(i = 0;i<5;i++)
    printf("child read: the %d people's age is %d\n",i+1,(*(p_map+i)).age);
    (*p_map).age = 100;
    munmap(p_map,sizeof(people)*10); //实际上,进程终止时,会自动解除映射。
    exit();
    }
    temp = 'a';
    for(i = 0;i<5;i++)
    {
    temp += 1;
    memcpy((*(p_map+i)).name, &temp,2);
    (*(p_map+i)).age=20+i;
    }
    sleep(5);
    printf( "parent read: the first people,s age is %d\n",(*p_map).age );
    printf("umap\n");
    munmap( p_map,sizeof(people)*10 );
    printf( "umap ok\n" );
    }

    考察程序的输出结果,体会父子进程匿名共享内存:

    1
    2
    3
    4
    5
    6
    7
    8
    child read: the 1 people's age is 20
    child read: the 2 people's age is 21
    child read: the 3 people's age is 22
    child read: the 4 people's age is 23
    child read: the 5 people's age is 24
    parent read: the first people,s age is 100
    umap
    umap ok

    四、对mmap()返回地址的访问

    前面对范例运行结构的讨论中已经提到,linux采用的是页式管理机制。对于用mmap()映射普通文件来说,进程会在自己的地址空间新增一块空间,空间大小由mmap()的len参数指定,注意,进程并不一定能够对全部新增空间都能进行有效访问。进程能够访问的有效地址大小取决于文件被映射部分的大小。简单的说,能够容纳文件被映射部分大小的最少页面个数决定了进程从mmap()返回的地址开始,能够有效访问的地址空间大小。超过这个空间大小,内核会根据超过的严重程度返回发送不同的信号给进程。可用如下图示说明:

    注意:文件被映射部分而不是整个文件决定了进程能够访问的空间大小,另外,如果指定文件的偏移部分,一定要注意为页面大小的整数倍。下面是对进程映射地址空间的访问范例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    #include <sys/mman.h>
    #include <sys/types.h>
    #include <fcntl.h>
    #include <unistd.h>
    typedef struct{
    char name[4];
    int age;
    }people;
    main(int argc, char** argv)
    {
    int fd,i;
    int pagesize,offset;
    people *p_map;

    pagesize = sysconf(_SC_PAGESIZE);
    printf("pagesize is %d\n",pagesize);
    fd = open(argv[1],O_CREAT|O_RDWR|O_TRUNC,00777);
    lseek(fd,pagesize*2-100,SEEK_SET);
    write(fd,"",1);
    offset = 0; //此处offset = 0编译成版本1;offset = pagesize编译成版本2
    p_map = (people*)mmap(NULL,pagesize*3,PROT_READ|PROT_WRITE,MAP_SHARED,fd,offset);
    close(fd);

    for(i = 1; i<10; i++)
    {
    (*(p_map+pagesize/sizeof(people)*i-2)).age = 100;
    printf("access page %d over\n",i);
    (*(p_map+pagesize/sizeof(people)*i-1)).age = 100;
    printf("access page %d edge over, now begin to access page %d\n",i, i+1);
    (*(p_map+pagesize/sizeof(people)*i)).age = 100;
    printf("access page %d over\n",i+1);
    }
    munmap(p_map,sizeof(people)*10);
    }

    如程序中所注释的那样,把程序编译成两个版本,两个版本主要体现在文件被映射部分的大小不同。文件的大小介于一个页面与两个页面之间(大小为:pagesize2-99),版本1的被映射部分是整个文件,版本2的文件被映射部分是文件大小减去一个页面后的剩余部分,不到一个页面大小(大小为:pagesize-99)。程序中试图访问每一个页面边界,两个版本都试图在进程空间中映射pagesize3的字节数。

    版本1的输出结果如下:

    1
    2
    3
    4
    5
    6
    7
    pagesize is 4096
    access page 1 over
    access page 1 edge over, now begin to access page 2
    access page 2 over
    access page 2 over
    access page 2 edge over, now begin to access page 3
    Bus error //被映射文件在进程空间中覆盖了两个页面,此时,进程试图访问第三个页面

    版本2的输出结果如下:

    1
    2
    3
    4
    pagesize is 4096
    access page 1 over
    access page 1 edge over, now begin to access page 2
    Bus error //被映射文件在进程空间中覆盖了一个页面,此时,进程试图访问第二个页面

    结论:采用系统调用mmap()实现进程间通信是很方便的,在应用层上接口非常简洁。内部实现机制区涉及到了linux存储管理以及文件系统等方面的内容,可以参考一下相关重要数据结构来加深理解。在本专题的后面部分,将介绍系统v共享内存的实现。

    系统V共享内存原理

    系统调用mmap()通过映射一个普通文件实现共享内存。系统V则是通过映射特殊文件系统shm中的文件实现进程间的共享内存通信。也就是说,每个共享内存区域对应特殊文件系统shm中的一个文件(这是通过shmid_kernel结构联系起来的),后面还将阐述。
    进程间需要共享的数据被放在一个叫做IPC共享内存区域的地方,所有需要访问该共享区域的进程都要把该共享区域映射到本进程的地址空间中去。系统V共享内存通过shmget获得或创建一个IPC共享内存区域,并返回相应的标识符。内核在保证shmget获得或创建一个共享内存区,初始化该共享内存区相应的shmid_kernel结构注同时,还将在特殊文件系统shm中,创建并打开一个同名文件,并在内存中建立起该文件的相应dentry及inode结构,新打开的文件不属于任何一个进程(任何进程都可以访问该共享内存区)。所有这一切都是系统调用shmget完成的。

    注:每一个共享内存区都有一个控制结构struct shmid_kernel,shmid_kernel是共享内存区域中非常重要的一个数据结构,它是存储管理和文件系统结合起来的桥梁,定义如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    struct shmid_kernel /* private to the kernel */
    {
    struct kern_ipc_perm shm_perm;
    struct file * shm_file;
    int id;
    unsigned long shm_nattch;
    unsigned long shm_segsz;
    time_t shm_atim;
    time_t shm_dtim;
    time_t shm_ctim;
    pid_t shm_cprid;
    pid_t shm_lprid;
    };

    该结构中最重要的一个域应该是shm_file,它存储了将被映射文件的地址。每个共享内存区对象都对应特殊文件系统shm中的一个文件,一般情况下,特殊文件系统shm中的文件是不能用read()、write()等方法访问的,当采取共享内存的方式把其中的文件映射到进程地址空间后,可直接采用访问内存的方式对其访问。

    这里我们采用下图表给出与系统V共享内存相关数据结构:

    正如消息队列和信号灯一样,内核通过数据结构struct ipc_ids shm_ids维护系统中的所有共享内存区域。上图中的shm_ids.entries变量指向一个ipc_id结构数组,而每个ipc_id结构数组中有个指向kern_ipc_perm结构的指针。到这里读者应该很熟悉了,对于系统V共享内存区来说,kern_ipc_perm的宿主是shmid_kernel结构,shmid_kernel是用来描述一个共享内存区域的,这样内核就能够控制系统中所有的共享区域。同时,在shmid_kernel结构的file类型指针shm_file指向文件系统shm中相应的文件,这样,共享内存区域就与shm文件系统中的文件对应起来。

    在创建了一个共享内存区域后,还要将它映射到进程地址空间,系统调用shmat()完成此项功能。由于在调用shmget()时,已经创建了文件系统shm中的一个同名文件与共享内存区域相对应,因此,调用shmat()的过程相当于映射文件系统shm中的同名文件过程,原理与mmap()大同小异。

    系统V共享内存API

    对于系统V共享内存,主要有以下几个API:shmget()、shmat()、shmdt()及shmctl()。

    1
    2
    #include <sys/ipc.h>
    #include <sys/shm.h>

    shmget()用来获得共享内存区域的ID,如果不存在指定的共享区域就创建相应的区域。shmat()把共享内存区域映射到调用进程的地址空间中去,这样,进程就可以方便地对共享区域进行访问操作。shmdt()调用用来解除进程对共享内存区域的映射。shmctl实现对共享内存区域的控制操作。这里我们不对这些系统调用作具体的介绍,读者可参考相应的手册页面,后面的范例中将给出它们的调用方法。

    注:shmget的内部实现包含了许多重要的系统V共享内存机制;shmat在把共享内存区域映射到进程空间时,并不真正改变进程的页表。当进程第一次访问内存映射区域访问时,会因为没有物理页表的分配而导致一个缺页异常,然后内核再根据相应的存储管理机制为共享内存映射区域分配相应的页表。

    系统V共享内存限制

    在/proc/sys/kernel/目录下,记录着系统V共享内存的一下限制,如一个共享内存区的最大字节数shmmax,系统范围内最大共享内存区标识符数shmmni等,可以手工对其调整,但不推荐这样做。

    系统V共享内存范例

    本部分将给出系统V共享内存API的使用方法,并对比分析系统V共享内存机制与mmap()映射普通文件实现共享内存之间的差异,首先给出两个进程通过系统V共享内存通信的范例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    /***** testwrite.c *******/
    #include <sys/ipc.h>
    #include <sys/shm.h>
    #include <sys/types.h>
    #include <unistd.h>
    typedef struct{
    char name[4];
    int age;
    } people;
    main(int argc, char** argv)
    {
    int shm_id,i;
    key_t key;
    char temp;
    people *p_map;
    char* name = "/dev/shm/myshm2";
    key = ftok(name,0);
    if(key==-1)
    perror("ftok error");
    shm_id=shmget(key,4096,IPC_CREAT);
    if(shm_id==-1)
    {
    perror("shmget error");
    return;
    }
    p_map=(people*)shmat(shm_id,NULL,0);
    temp='a';
    for(i = 0;i<10;i++)
    {
    temp+=1;
    memcpy((*(p_map+i)).name,&temp,1);
    (*(p_map+i)).age=20+i;
    }
    if(shmdt(p_map)==-1)
    perror(" detach error ");
    }
    /********** testread.c ************/
    #include <sys/ipc.h>
    #include <sys/shm.h>
    #include <sys/types.h>
    #include <unistd.h>
    typedef struct{
    char name[4];
    int age;
    } people;
    main(int argc, char** argv)
    {
    int shm_id,i;
    key_t key;
    people *p_map;
    char* name = "/dev/shm/myshm2";
    key = ftok(name,0);
    if(key == -1)
    perror("ftok error");
    shm_id = shmget(key,4096,IPC_CREAT);
    if(shm_id == -1)
    {
    perror("shmget error");
    return;
    }
    p_map = (people*)shmat(shm_id,NULL,0);
    for(i = 0;i<10;i++)
    {
    printf( "name:%s\n",(*(p_map+i)).name );
    printf( "age %d\n",(*(p_map+i)).age );
    }
    if(shmdt(p_map) == -1)
    perror(" detach error ");
    }

    testwrite.c创建一个系统V共享内存区,并在其中写入格式化数据;testread.c访问同一个系统V共享内存区,读出其中的格式化数据。分别把两个程序编译为testwrite及testread,先后执行./testwrite及./testread 则./testread输出结果如下:

    1
    2
    name: b	age 20;	name: c	age 21;	name: d	age 22;	name: e	age 23;	name: f	age 24;
    name: g age 25; name: h age 26; name: I age 27; name: j age 28; name: k age 29;

    通过对试验结果分析,对比系统V与mmap()映射普通文件实现共享内存通信,可以得出如下结论:
    1、 系统V共享内存中的数据,从来不写入到实际磁盘文件中去;而通过mmap()映射普通文件实现的共享内存通信可以指定何时将数据写入磁盘文件中。 注:前面讲到,系统V共享内存机制实际是通过映射特殊文件系统shm中的文件实现的,文件系统shm的安装点在交换分区上,系统重新引导后,所有的内容都丢失。

    2、 系统V共享内存是随内核持续的,即使所有访问共享内存的进程都已经正常终止,共享内存区仍然存在(除非显式删除共享内存),在内核重新引导之前,对该共享内存区域的任何改写操作都将一直保留。

    3、 通过调用mmap()映射普通文件进行进程间通信时,一定要注意考虑进程何时终止对通信的影响。而通过系统V共享内存实现通信的进程则不然。 注:这里没有给出shmctl的使用范例,原理与消息队列大同小异。

    结论:

    共享内存允许两个或多个进程共享一给定的存储区,因为数据不需要来回复制,所以是最快的一种进程间通信机制。共享内存可以通过mmap()映射普通文件(特殊情况下还可以采用匿名映射)机制实现,也可以通过系统V共享内存机制实现。应用接口和原理很简单,内部机制复杂。为了实现更安全通信,往往还与信号灯等同步机制共同使用。

    共享内存涉及到了存储管理以及文件系统等方面的知识,深入理解其内部机制有一定的难度,关键还要紧紧抓住内核使用的重要数据结构。系统V共享内存是以文件的形式组织在特殊文件系统shm中的。通过shmget可以创建或获得共享内存的标识符。取得共享内存标识符后,要通过shmat将这个内存区映射到本进程的虚拟地址空间。

    本文转载自这里

    管道

    管道及有名管道

    管道和有名管道是最早的进程间通信机制之一,管道可用于具有亲缘关系进程间的通信,有名管道克服了管道没有名字的限制,因此,除具有管道所具有的功能外,它还允许无亲缘关系进程间的通信。

    管道概述及相关API应用

    管道相关的关键概念

    管道是Linux支持的最初Unix IPC形式之一,具有以下特点:

  • 管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道;

  • 只能用于父子进程或者兄弟进程之间(具有亲缘关系的进程);

  • 单独构成一种独立的文件系统:管道对于管道两端的进程而言,就是一个文件,但它不是普通的文件,它不属于某种文件系统,而是自立门户,单独构成一种文件系统,并且只存在与内存中。

  • 数据的读出和写入:一个进程向管道中写的内容被管道另一端的进程读出。写入的内容每次都添加在管道缓冲区的末尾,并且每次都是从缓冲区的头部读出数据。
  • 管道的创建:

    1
    2
    #include <unistd.h>
    int pipe(int fd[2])

    该函数创建的管道的两端处于一个进程中间,在实际应用中没有太大意义,因此,一个进程在由pipe()创建管道后,一般再fork一个子进程,然后通过管道实现父子进程间的通信(因此也不难推出,只要两个进程中存在亲缘关系,这里的亲缘关系指的是具有共同的祖先,都可以采用管道方式来进行通信)。

    管道的读写规则:

    管道两端可分别用描述字fd[0]以及fd[1]来描述,需要注意的是,管道的两端是固定了任务的。即一端只能用于读,由描述字fd[0]表示,称其为管道读端;另一端则只能用于写,由描述字fd[1]来表示,称其为管道写端。如果试图从管道写端读取数据,或者向管道读端写入数据都将导致错误发生。一般文件的I/O函数都可以用于管道,如close、read、write等等。

    从管道中读取数据:

  • 如果管道的写端不存在,则认为已经读到了数据的末尾,读函数返回的读出字节数为0;

  • 当管道的写端存在时,如果请求的字节数目大于PIPE_BUF,则返回管道中现有的数据字节数,如果请求的字节数目不大于PIPE_BUF,则返回管道中现有数据字节数(此时,管道中数据量小于请求的数据量);或者返回请求的字节数(此时,管道中数据量不小于请求的数据量)。注:(PIPE_BUF在include/linux/limits.h中定义,不同的内核版本可能会有所不同。Posix.1要求PIPE_BUF至少为512字节,red hat 7.2中为4096)。
  • 关于管道的读规则验证:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    /**************
    * readtest.c *
    **************/
    #include <unistd.h>
    #include <sys/types.h>
    #include <errno.h>
    main()
    {
    int pipe_fd[2];
    pid_t pid;
    char r_buf[100];
    char w_buf[4];
    char* p_wbuf;
    int r_num;
    int cmd;

    memset(r_buf,0,sizeof(r_buf));
    memset(w_buf,0,sizeof(r_buf));
    p_wbuf=w_buf;
    if(pipe(pipe_fd)<0)
    {
    printf("pipe create error\n");
    return -1;
    }

    if((pid=fork())==0)
    {
    printf("\n");
    close(pipe_fd[1]);
    sleep(3);//确保父进程关闭写端
    r_num=read(pipe_fd[0],r_buf,100);
    printf( "read num is %d the data read from the pipe is %d\n",r_num,atoi(r_buf));

    close(pipe_fd[0]);
    exit();
    }
    else if(pid>0)

    {
    close(pipe_fd[0]);//read
    strcpy(w_buf,"111");
    if(write(pipe_fd[1],w_buf,4)!=-1)
    printf("parent write over\n");
    close(pipe_fd[1]);//write
    printf("parent close fd[1] over\n");
    sleep(10);
    }
    }
    /**************************************************
    * 程序输出结果:
    * parent write over
    * parent close fd[1] over
    * read num is 4 the data read from the pipe is 111
    * 附加结论:
    * 管道写端关闭后,写入的数据将一直存在,直到读出为止.
    ****************************************************/

    向管道中写入数据:

  • 向管道中写入数据时,linux将不保证写入的原子性,管道缓冲区一有空闲区域,写进程就会试图向管道写入数据。如果读进程不读走管道缓冲区中的数据,那么写操作将一直阻塞。
    注:只有在管道的读端存在时,向管道中写入数据才有意义。否则,向管道中写入数据的进程将收到内核传来的SIFPIPE信号,应用程序可以处理该信号,也可以忽略(默认动作则是应用程序终止)。
  • 对管道的写规则的验证1:写端对读端存在的依赖性

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    #include <unistd.h>
    #include <sys/types.h>
    main()
    {
    int pipe_fd[2];
    pid_t pid;
    char r_buf[4];
    char* w_buf;
    int writenum;
    int cmd;

    memset(r_buf,0,sizeof(r_buf));
    if(pipe(pipe_fd)<0)
    {
    printf("pipe create error\n");
    return -1;
    }

    if((pid=fork())==0)
    {
    close(pipe_fd[0]);
    close(pipe_fd[1]);
    sleep(10);
    exit();
    }
    else if(pid>0)
    {
    sleep(1); //等待子进程完成关闭读端的操作
    close(pipe_fd[0]);//write
    w_buf="111";
    if((writenum=write(pipe_fd[1],w_buf,4))==-1)
    printf("write to pipe error\n");
    else
    printf("the bytes write to pipe is %d \n", writenum);

    close(pipe_fd[1]);
    }
    }

    则输出结果为: Broken pipe,原因就是该管道以及它的所有fork()产物的读端都已经被关闭。如果在父进程中保留读端,即在写完pipe后,再关闭父进程的读端,也会正常写入pipe,读者可自己验证一下该结论。因此,在向管道写入数据时,至少应该存在某一个进程,其中管道读端没有被关闭,否则就会出现上述错误(管道断裂,进程收到了SIGPIPE信号,默认动作是进程终止)

    对管道的写规则的验证2:linux不保证写管道的原子性验证

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    #include <unistd.h>
    #include <sys/types.h>
    #include <errno.h>
    main(int argc,char**argv)
    {
    int pipe_fd[2];
    pid_t pid;
    char r_buf[4096];
    char w_buf[4096*2];
    int writenum;
    int rnum;
    memset(r_buf,0,sizeof(r_buf));
    if(pipe(pipe_fd)<0)
    {
    printf("pipe create error\n");
    return -1;
    }

    if((pid=fork())==0)
    {
    close(pipe_fd[1]);
    while(1)
    {
    sleep(1);
    rnum=read(pipe_fd[0],r_buf,1000);
    printf("child: readnum is %d\n",rnum);
    }
    close(pipe_fd[0]);

    exit();
    }
    else if(pid>0)
    {
    close(pipe_fd[0]);//write
    memset(r_buf,0,sizeof(r_buf));
    if((writenum=write(pipe_fd[1],w_buf,1024))==-1)
    printf("write to pipe error\n");
    else
    printf("the bytes write to pipe is %d \n", writenum);
    writenum=write(pipe_fd[1],w_buf,4096);
    close(pipe_fd[1]);
    }
    }
    输出结果:
    the bytes write to pipe 1000
    the bytes write to pipe 1000 //注意,此行输出说明了写入的非原子性
    the bytes write to pipe 1000
    the bytes write to pipe 1000
    the bytes write to pipe 1000
    the bytes write to pipe 120 //注意,此行输出说明了写入的非原子性
    the bytes write to pipe 0
    the bytes write to pipe 0
    ......

    结论:

    写入数目小于4096时写入是非原子的!
    如果把父进程中的两次写入字节数都改为5000,则很容易得出下面结论:
    写入管道的数据量大于4096字节时,缓冲区的空闲空间将被写入数据(补齐),直到写完所有数据为止,如果没有进程读数据,则一直阻塞。

    管道应用实例:

    实例一:用于shell

    管道可用于输入输出重定向,它将一个命令的输出直接定向到另一个命令的输入。比如,当在某个shell程序(Bourne shell或C shell等)键入who│wc -l后,相应shell程序将创建who以及wc两个进程和这两个进程间的管道。考虑下面的命令行:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    $kill -l 运行结果见 附一。

    $kill -l | grep SIGRTMIN 运行结果如下:

    30) SIGPWR 31) SIGSYS 32) SIGRTMIN 33) SIGRTMIN+1
    34) SIGRTMIN+2 35) SIGRTMIN+3 36) SIGRTMIN+4 37) SIGRTMIN+5
    38) SIGRTMIN+6 39) SIGRTMIN+7 40) SIGRTMIN+8 41) SIGRTMIN+9
    42) SIGRTMIN+10 43) SIGRTMIN+11 44) SIGRTMIN+12 45) SIGRTMIN+13
    46) SIGRTMIN+14 47) SIGRTMIN+15 48) SIGRTMAX-15 49) SIGRTMAX-14

    实例二:用于具有亲缘关系的进程间通信

    下面例子给出了管道的具体应用,父进程通过管道发送一些命令给子进程,子进程解析命令,并根据命令作相应处理。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    #include <unistd.h>
    #include <sys/types.h>
    main()
    {
    int pipe_fd[2];
    pid_t pid;
    char r_buf[4];
    char** w_buf[256];
    int childexit=0;
    int i;
    int cmd;

    memset(r_buf,0,sizeof(r_buf));
    if(pipe(pipe_fd)<0)
    {
    printf("pipe create error\n");
    return -1;
    }
    if((pid=fork())==0)
    //子进程:解析从管道中获取的命令,并作相应的处理
    {
    printf("\n");
    close(pipe_fd[1]);
    sleep(2);

    while(!childexit)
    {
    read(pipe_fd[0],r_buf,4);
    cmd=atoi(r_buf);
    if(cmd==0)
    {
    printf("child: receive command from parent over\n now child process exit\n");
    childexit=1;
    }

    else if(handle_cmd(cmd)!=0)
    return;
    sleep(1);
    }
    close(pipe_fd[0]);
    exit();
    }
    else if(pid>0)
    //parent: send commands to child
    {
    close(pipe_fd[0]);
    w_buf[0]="003";
    w_buf[1]="005";
    w_buf[2]="777";
    w_buf[3]="000";
    for(i=0;i<4;i++)
    write(pipe_fd[1],w_buf[i],4);
    close(pipe_fd[1]);
    }
    }
    //下面是子进程的命令处理函数(特定于应用):
    int handle_cmd(int cmd)
    {

    if((cmd<0)||(cmd>256))
    //suppose child only support 256 commands
    {
    printf("child: invalid command \n");
    return -1;
    }
    printf("child: the cmd from parent is %d\n", cmd);
    return 0;
    }

    管道的局限性

    管道的主要局限性正体现在它的特点上:

  • 只支持单向数据流;

  • 只能用于具有亲缘关系的进程之间;

  • 没有名字;

  • 管道的缓冲区是有限的(管道制存在于内存中,在管道创建时,为缓冲区分配一个页面大小);

  • 管道所传送的是无格式字节流,这就要求管道的读出方和写入方必须事先约定好数据的格式,比如多少字节算作一个消息(或命令、或记录)等等;
  • 有名管道概述及相关API应用

    有名管道相关的关键概念

    管道应用的一个重大限制是它没有名字,因此,只能用于具有亲缘关系的进程间通信,在有名管道(named pipe或FIFO)提出后,该限制得到了克服。FIFO不同于管道之处在于它提供一个路径名与之关联,以FIFO的文件形式存在于文件系统中。这样,即使与FIFO的创建进程不存在亲缘关系的进程,只要可以访问该路径,就能够彼此通过FIFO相互通信(能够访问该路径的进程以及FIFO的创建进程之间),因此,通过FIFO不相关的进程也能交换数据。值得注意的是,FIFO严格遵循先进先出(first in first out),对管道及FIFO的读总是从开始处返回数据,对它们的写则把数据添加到末尾。它们不支持诸如lseek()等文件定位操作。

    有名管道的创建

    1
    2
    3
    #include <sys/types.h>
    #include <sys/stat.h>
    int mkfifo(const char * pathname, mode_t mode)

    该函数的第一个参数是一个普通的路径名,也就是创建后FIFO的名字。第二个参数与打开普通文件的open()函数中的mode 参数相同。 如果mkfifo的第一个参数是一个已经存在的路径名时,会返回EEXIST错误,所以一般典型的调用代码首先会检查是否返回该错误,如果确实返回该错误,那么只要调用打开FIFO的函数就可以了。一般文件的I/O函数都可以用于FIFO,如close、read、write等等。

    有名管道的打开规则

    有名管道比管道多了一个打开操作:open。

    FIFO的打开规则:

    如果当前打开操作是为读而打开FIFO时,若已经有相应进程为写而打开该FIFO,则当前打开操作将成功返回;否则,可能阻塞直到有相应进程为写而打开该FIFO(当前打开操作设置了阻塞标志);或者,成功返回(当前打开操作没有设置阻塞标志)。

    如果当前打开操作是为写而打开FIFO时,如果已经有相应进程为读而打开该FIFO,则当前打开操作将成功返回;否则,可能阻塞直到有相应进程为读而打开该FIFO(当前打开操作设置了阻塞标志);或者,返回ENXIO错误(当前打开操作没有设置阻塞标志)。

    对打开规则的验证:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    #include <sys/types.h>
    #include <sys/stat.h>
    #include <errno.h>
    #include <fcntl.h>
    #define FIFO_SERVER "/tmp/fifoserver"
    int handle_client(char*);
    main(int argc,char** argv)
    {
    int r_rd;
    int w_fd;
    pid_t pid;
    if((mkfifo(FIFO_SERVER,O_CREAT|O_EXCL)<0)&&(errno!=EEXIST))
    printf("cannot create fifoserver\n");
    handle_client(FIFO_SERVER);

    }
    int handle_client(char* arg)
    {

    int ret;
    ret=w_open(arg);
    switch(ret)
    {
    case 0:
    {
    printf("open %s error\n",arg);
    printf("no process has the fifo open for reading\n");
    return -1;
    }
    case -1:
    {
    printf("something wrong with open the fifo except for ENXIO");
    return -1;
    }
    case 1:
    {
    printf("open server ok\n");
    return 1;
    }
    default:
    {
    printf("w_no_r return ----\n");
    return 0;
    }
    }
    unlink(FIFO_SERVER);
    }
    int w_open(char*arg)
    //0 open error for no reading
    //-1 open error for other reasons
    //1 open ok
    {

    if(open(arg,O_WRONLY|O_NONBLOCK,0)==-1)
    { if(errno==ENXIO)
    {
    return 0;
    }
    else
    return -1;
    }
    return 1;

    }

    有名管道的读写规则

    从FIFO中读取数据:

    约定:如果一个进程为了从FIFO中读取数据而阻塞打开FIFO,那么称该进程内的读操作为设置了阻塞标志的读操作。

  • 如果有进程写打开FIFO,且当前FIFO内没有数据,则对于设置了阻塞标志的读操作来说,将一直阻塞。对于没有设置阻塞标志读操作来说则返回-1,当前errno值为EAGAIN,提醒以后再试。

  • 对于设置了阻塞标志的读操作说,造成阻塞的原因有两种:当前FIFO内有数据,但有其它进程在读这些数据;另外就是FIFO内没有数据。解阻塞的原因则是FIFO中有新的数据写入,不论信写入数据量的大小,也不论读操作请求多少数据量。

  • 读打开的阻塞标志只对本进程第一个读操作施加作用,如果本进程内有多个读操作序列,则在第一个读操作被唤醒并完成读操作后,其它将要执行的读操作将不再阻塞,即使在执行读操作时,FIFO中没有数据也一样(此时,读操作返回0)。

  • 如果没有进程写打开FIFO,则设置了阻塞标志的读操作会阻塞。
  • 注:如果FIFO中有数据,则设置了阻塞标志的读操作不会因为FIFO中的字节数小于请求读的字节数而阻塞,此时,读操作会返回FIFO中现有的数据量。

    向FIFO中写入数据:

    约定:如果一个进程为了向FIFO中写入数据而阻塞打开FIFO,那么称该进程内的写操作为设置了阻塞标志的写操作。

    对于设置了阻塞标志的写操作:

  • 当要写入的数据量不大于PIPE_BUF时,linux将保证写入的原子性。如果此时管道空闲缓冲区不足以容纳要写入的字节数,则进入睡眠,直到当缓冲区中能够容纳要写入的字节数时,才开始进行一次性写操作。

  • 当要写入的数据量大于PIPE_BUF时,linux将不再保证写入的原子性。FIFO缓冲区一有空闲区域,写进程就会试图向管道写入数据,写操作在写完所有请求写的数据后返回。
  • 对于没有设置阻塞标志的写操作:

  • 当要写入的数据量大于PIPE_BUF时,linux将不再保证写入的原子性。在写满所有FIFO空闲缓冲区后,写操作返回。

  • 当要写入的数据量不大于PIPE_BUF时,linux将保证写入的原子性。如果当前FIFO空闲缓冲区能够容纳请求写入的字节数,写完后成功返回;如果当前FIFO空闲缓冲区不能够容纳请求写入的字节数,则返回EAGAIN错误,提醒以后再写;
  • 对FIFO读写规则的验证:

    下面提供了两个对FIFO的读写程序,适当调节程序中的很少地方或者程序的命令行参数就可以对各种FIFO读写规则进行验证。

    程序1:写FIFO的程序
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    #include <sys/types.h>
    #include <sys/stat.h>
    #include <errno.h>
    #include <fcntl.h>
    #define FIFO_SERVER "/tmp/fifoserver"
    main(int argc,char** argv)
    //参数为即将写入的字节数
    {
    int fd;
    char w_buf[4096*2];
    int real_wnum;
    memset(w_buf,0,4096*2);
    if((mkfifo(FIFO_SERVER,O_CREAT|O_EXCL)<0)&&(errno!=EEXIST))
    printf("cannot create fifoserver\n");
    if(fd==-1)
    if(errno==ENXIO)
    printf("open error; no reading process\n");

    fd=open(FIFO_SERVER,O_WRONLY|O_NONBLOCK,0);
    //设置非阻塞标志
    //fd=open(FIFO_SERVER,O_WRONLY,0);
    //设置阻塞标志
    real_wnum=write(fd,w_buf,2048);
    if(real_wnum==-1)
    {
    if(errno==EAGAIN)
    printf("write to fifo error; try later\n");
    }
    else
    printf("real write num is %d\n",real_wnum);
    real_wnum=write(fd,w_buf,5000);
    //5000用于测试写入字节大于4096时的非原子性
    //real_wnum=write(fd,w_buf,4096);
    //4096用于测试写入字节不大于4096时的原子性

    if(real_wnum==-1)
    if(errno==EAGAIN)
    printf("try later\n");
    }
    程序2:与程序1一起测试写FIFO的规则,第一个命令行参数是请求从FIFO读出的字节数
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    #include <sys/types.h>
    #include <sys/stat.h>
    #include <errno.h>
    #include <fcntl.h>
    #define FIFO_SERVER "/tmp/fifoserver"
    main(int argc,char** argv)
    {
    char r_buf[4096*2];
    int fd;
    int r_size;
    int ret_size;
    r_size=atoi(argv[1]);
    printf("requred real read bytes %d\n",r_size);
    memset(r_buf,0,sizeof(r_buf));
    fd=open(FIFO_SERVER,O_RDONLY|O_NONBLOCK,0);
    //fd=open(FIFO_SERVER,O_RDONLY,0);
    //在此处可以把读程序编译成两个不同版本:阻塞版本及非阻塞版本
    if(fd==-1)
    {
    printf("open %s for read error\n");
    exit();
    }
    while(1)
    {

    memset(r_buf,0,sizeof(r_buf));
    ret_size=read(fd,r_buf,r_size);
    if(ret_size==-1)
    if(errno==EAGAIN)
    printf("no data avlaible\n");
    printf("real read bytes %d\n",ret_size);
    sleep(1);
    }
    pause();
    unlink(FIFO_SERVER);
    }

    程序应用说明:

    把读程序编译成两个不同版本:

  • 阻塞读版本:br

  • 以及非阻塞读版本nbr
  • 把写程序编译成两个四个版本:

  • 非阻塞且请求写的字节数大于PIPE_BUF版本:nbwg

  • 非阻塞且请求写的字节数不大于PIPE_BUF版本:版本nbw

  • 阻塞且请求写的字节数大于PIPE_BUF版本:bwg

  • 阻塞且请求写的字节数不大于PIPE_BUF版本:版本bw
  • 下面将使用br、nbr、w代替相应程序中的阻塞读、非阻塞读

    验证阻塞写操作:
    1.当请求写入的数据量大于PIPE_BUF时的非原子性:
    nbr 1000
    bwg

    2.当请求写入的数据量不大于PIPE_BUF时的原子性:
    nbr 1000
    bw

    验证非阻塞写操作:
    1.当请求写入的数据量大于PIPE_BUF时的非原子性:
    nbr 1000
    nbwg

    2.请求写入的数据量不大于PIPE_BUF时的原子性:
    nbr 1000
    nbw

    不管写打开的阻塞标志是否设置,在请求写入的字节数大于4096时,都不保证写入的原子性。但二者有本质区别:

    对于阻塞写来说,写操作在写满FIFO的空闲区域后,会一直等待,直到写完所有数据为止,请求写入的数据最终都会写入FIFO;

    而非阻塞写则在写满FIFO的空闲区域后,就返回(实际写入的字节数),所以有些数据最终不能够写入。

    对于读操作的验证则比较简单,不再讨论。

    小结:

    管道常用于两个方面:
    (1)在shell中时常会用到管道(作为输入输入的重定向),在这种应用方式下,管道的创建对于用户来说是透明的;
    (2)用于具有亲缘关系的进程间通信,用户自己创建管道,并完成读写操作。

    FIFO可以说是管道的推广,克服了管道无名字的限制,使得无亲缘关系的进程同样可以采用先进先出的通信机制进行通信。

    管道和FIFO的数据是字节流,应用程序之间必须事先确定特定的传输”协议”,采用传播具有特定意义的消息。

    要灵活应用管道及FIFO,理解它们的读写规则是关键。

    以上内容转自这里

    redis单机数据库

    redis将数据库结构都保存在服务器状态的redis.h/redisServer数据结构中的db数组中,db数组的每一个元素都是一个redis.h/redisDb结构,每一个redisDb结构代表一个数据库:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    struct redisServer{

    //服务器初始化数据库数量
    int dbnum;

    //保存数据库的数组
    redisDb *db;
    ...
    ...
    };

    redisDb的结构如下:

    1
    2
    3
    4
    5
    6
    7
    typedef struct redisDb{
    //数据库键空间,保存着数据库中的所有键值对
    dict *dict;

    ...
    ...
    }redisDb;

    键空间和用户所见的数据库是直接对应的:

    1. 键空间的键也就是数据库的键,每一个键都是一个字符串对象
    2. 键空间的值也就是数据库的值,每个值可以是字符串对象、列表对象、哈希对象等redis中的任何对象。

    键的生存时间

    通过expire和pexpire命令,客户端可以以秒或者毫秒精度为数据库中的某个键设置生存时间:

    1
    2
    3
    4
    5
    6
    7
    8
    redis> set key 9
    OK

    redis> expire key 5 //5秒后过期
    (integer) 1

    redis> get key //5秒后重新获取key时,会出现错误
    (nil)

    expireat和pexpireat命令用来设置在某个时间戳后,键超时:

    1
    2
    3
    4
    5
    6
    redis> set key value
    OK

    //设置在特定时间戳后过期
    redis> expireat key 1377257300
    (integer) 1

    可以使用persist来取消键的过期时间:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    redis> expire key 5
    OK

    redis> TTL key
    (integer) ****

    redis> persist key
    (integer) 1

    redis> TTL key
    (integer) -1

    redis使用expires字典来保存数据库中键的过期时间,对于过期键,需要删除,否则的话会占用内存。对于过期键的删除,有三种可能的策略:

    1. 定时删除:在设置键的过期时间时,创建一个定时器,让定时器在键的过期时间来临时,立即执行删除操作。该策略对内存是最友好的,但是对CPU最不友好
    2. 惰性删除:放任键过期不管,但访问到键时,才对键执行删除操作,如果过期的键没有被访问到,那么键就直接存在内存。该策略对CPU最友好,但是对内存不友好
    3. 定期删除:每隔一段时间,程序就对数据库进行一次检查,删除过期的键。至于要删除多少,以及检查多少个数据库,则由算法决定
      redis采用惰性和定期删除两种策略,通过配合使用这两种策略,服务器可以很好的在合理使用CPU时间和避免浪费内存空间之间取得平衡。

    redis服务器采用reactor的模型,是一个事件驱动程序,在redis服务器中,有两种事件:文件事件和时间事件。文件事件就是所建立的socket而时间事件就是redis自己需要的,用来执行一些定时操作。
    redis采用IO复用的模式来处理文件事件,对于时间事件,由于目前redis的时间事件很少,所以redis采用链表的形式来保存所有的时间事件。每次遍历链表,执行所有超时的时间事件。

    因为服务器中同时存在文件事件和时间事件两种类型,所以服务器必须对这两种事件进行调度,决定何时应该处理文件事件和时间事件,以及花多少时间来处理他们。redis会遍历时间事件,获得最近的定时事件的时间,如果定时事件已经到达,那么时间为0,redis以该时间来决定监听IO复用的超时时间。(其实这里如果时间事件是平凡执行的,那么redis会在时间事件上busy,导致文件事件饿死,但是由于时间事件是redis自己加的,不对外开放,所以不会存在恶意时间事件)。

    redis发布与订阅

    redis可以发布与订阅频道,有两种订阅方式,一种是具体频道,一种是模式匹配频道。
    对于具体频道,redis采用了字典来保存具体频道与订阅客户端的数据,key为频道,value为订阅的客户端,采用链表的形式保存。对于模式频道,redis直接采用链表的方式来保存模式客户端。每当有消息发布,redis根据消息所属的频道来通过字典找到客户端链表,遍历链表,发送消息。接着遍历模式链表,查找匹配的模式频道,发送消息给客户端。
    具体的代码如下所示:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    /* Publish a message
    *
    * 将 message 发送到所有订阅频道 channel 的客户端,
    * 以及所有订阅了和 channel 频道匹配的模式的客户端。
    */

    int pubsubPublishMessage(robj *channel, robj *message) {
    int receivers = 0;
    dictEntry *de;
    listNode *ln;
    listIter li;

    /* Send to clients listening for that channel */
    // 取出包含所有订阅频道 channel 的客户端的链表
    // 并将消息发送给它们
    de = dictFind(server.pubsub_channels,channel);
    if (de) {
    list *list = dictGetVal(de);
    listNode *ln;
    listIter li;

    // 遍历客户端链表,将 message 发送给它们
    listRewind(list,&li);
    while ((ln = listNext(&li)) != NULL) {
    redisClient *c = ln->value;

    // 回复客户端。
    // 示例:
    // 1) "message"
    // 2) "xxx"
    // 3) "hello"
    addReply(c,shared.mbulkhdr[3]);
    // "message" 字符串
    addReply(c,shared.messagebulk);
    // 消息的来源频道
    addReplyBulk(c,channel);
    // 消息内容
    addReplyBulk(c,message);

    // 接收客户端计数
    receivers++;
    }
    }

    /* Send to clients listening to matching channels */
    // 将消息也发送给那些和频道匹配的模式
    if (listLength(server.pubsub_patterns)) {

    // 遍历模式链表
    listRewind(server.pubsub_patterns,&li);
    channel = getDecodedObject(channel);
    while ((ln = listNext(&li)) != NULL) {

    // 取出 pubsubPattern
    pubsubPattern *pat = ln->value;

    // 如果 channel 和 pattern 匹配
    // 就给所有订阅该 pattern 的客户端发送消息
    if (stringmatchlen((char*)pat->pattern->ptr,
    sdslen(pat->pattern->ptr),
    (char*)channel->ptr,
    sdslen(channel->ptr),0)) {

    // 回复客户端
    // 示例:
    // 1) "pmessage"
    // 2) "*"
    // 3) "xxx"
    // 4) "hello"
    addReply(pat->client,shared.mbulkhdr[4]);
    addReply(pat->client,shared.pmessagebulk);
    addReplyBulk(pat->client,pat->pattern);
    addReplyBulk(pat->client,channel);
    addReplyBulk(pat->client,message);

    // 对接收消息的客户端进行计数
    receivers++;
    }
    }

    decrRefCount(channel);
    }

    // 返回计数
    return receivers;
    }

    redis数据结构

    redis的数据结构主要有以下几种:SDS(simple dynamic string),embstr(编码的简单动态字符串),字典,链表(双端链表),压缩列表,整数集合,跳表。

    SDS的主要结构为:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    struct sdshdr{
    //记录buffer中的存储数据的实际长度
    int len;
    //记录buffer当前还剩下的长度
    int free;

    //buffer,实际存储数据的数组,会在存储数据的最后面加一个'\0',以适应于系统的一些字符串处理函数
    char buf[];
    }

    SDS与C字符串的区别:

    1. 属性len可以使得SDS能够在常数时间内返回字符串的长度
    2. 杜绝缓冲区的溢出,C字符串不记录本身的长度,所以在字符串的复制、拼接(strcat)上面会出现溢出,而SDS有free属性,可以根据属性来确定还有没有空间可以允许安全的字符串操作,没有的话会自动的申请更大的内存空间,并复制原来的数据。
    3. 减少修改字符串所带来的内存重分配次数。由于C字符串没有预分配策略,所以每一次的字符串增加或减少操作都会造成内存的重分配。而SDS有内存预分配策略,所以可以减少相应的内存重分配次数。
    4. 二进制安全的。因为SDS是根据len来确定所存储的字符串的长度的,而不是硬性的根据’\0’来决定字符串的结尾,所以对于二进制等会在数据中出现’\0’的数据,SDS是安全的,可以完整的读取以及存储数据,而C字符串却不能。

    链表

    redis的链表采用的是双向链表的方式,每一个链表节点都有指向上一个和下一个节点的指针。链表由list数据结构来表示,主要负责管理链表,有链表头、链表尾、链表长度等属性。链表的节点就是简单的几个属性:节点值、上一个节点指针、下一个节点指针。

    字典

    redis使用字典来表示数据库,也用来作为哈希键的底层实现。哈希表采用数组加链表的方式来实现以及解决哈希冲突的问题。字典的结构与链表一样,有两个数据结构组成。一个是dictht,用来表示整个哈希表的具体信息,另一个数dictEntry,为哈希表的具体节点值,用来存储key
    和value数据。每一个dictEntry都有一个对应的指针,来指向下一个哈希冲突的dictEntry。在dictht中,有两个dictEntry** ht[2]元素, 一个用来指向真是的哈希数组,一个在平时的值为nullptr。在哈希数组达到一定的饱和度或是太过于稀疏的时候,redis会进行rehash操作,此时会重新生成哈希数组,指向nullptr的属性会指向该新的数组起始地址,将原来的哈希表数据剪切到新的空间里面,剪切完成后,会将旧的属性赋值为nullptr。

    rehash的操作有两种策略,一种是一次性操作,一次性将原有的数据剪切到新的空间里面。另外一种是渐进式的rehash,每次对于字典的操作,都会将对应的key以及value剪切到新的空间里面,该策略在对字典的操作,比如查找时,需要新旧两个哈希数组一起参与,在久的找不到的情况下查找新的。

    跳跃表

    与链表一样,跳跃表也由两种数据结构组成,redis.h/zskiplistNode和redis.h/zskiplist。zskiplist用来表示整个跳跃表的具体信息,zskiplistNode用来表示跳跃表的节点。

    整数集合

    整数结合用来保存整数值集合。具体的数据结构为:

    1
    2
    3
    4
    5
    6
    7
    8
    typedef struct inset{
    //编码方式:INSET_ENC_INT16,INSET_ENC_INT32,INSET_ENC_INT64,用来表示content数组里面的数据的真实类型
    uint32_t encoding;
    //集合的元素数量
    uint32_t length;
    //保存集合的数组,数组里面存储的真实数据会根据encoding属性来确定,并不是根据content的int8_t类型来确定的。
    int8_t contents[];
    }inset;

    content数组中,各个元素按照值的大小递增排序,并且只出现一次。
    当向整数集合插入新的元素,并且新元素的类型比原有的类型还大时,redis会对整数集合进行编码升级,所有的数据的类型都会升级为新的类型。所以需要对conetne执行新的内存操作以及数据剪切。由于新元素的值要么大于content中的任何值,要么小于content中的任何值,所以在新的content里面,新元素的位置要么在最后,要么在最前。

    升级的好处
    1. 通过升级,整数集合可以存储不同类型的整数,由于C语言是静态类型语言,如果不升级的话,就需要有不同类型的数据结构。通过升级就可以直接使用一种数据结构来表示多种类型整数。
    2. 节约内存。为了能够存储多种数据类型,只在需要的时候才进行升级,这样可以节约内存,不然的话,需要一开始就定义最大的整数类型作为content
      的元素类型。

    整数集合是没有降级的,只有升级,一旦升级后,编码就会一直保持升级后的编码,即使content中的元素值很小,也不会执行降级。

    压缩列表

    压缩列表是列表键和哈希键的底层实现之一。当一个列表键只包含少量列表项,并且每个列表项要么就是小整数值,要么就是长度比较短的字符串,那么redis就会使用压缩列表来做列表键的底层实现。另外,当一个哈希键只包含少量键值对时,并且每个键值对的键和值要么就是小整数值,要么就是长度比较短的字符串,那么redis就会使用压缩列表来做哈希键的底层实现。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    redis> RPUSH lst 1 2 3 4 "hello" "world"
    (integer)6

    redis> OBJECT ENCODING lst
    "ziplist"


    redis> HMSET profile "name" "Jack" "age" 28 "Job" "Programmer"
    OK

    redis> OBJECT ENCODING profile
    "ziplist"

    redis对象

    redis并没有直接使用这些数据结构来实现键值对数据库,而是基于这些数据结构创建了一个对象系统,这个系统包含字符串对象、列表对象、哈希对象、集合对象以及有序集合对象这五种类型的对象,每种对象都用到了至少一种redis中的数据结构。

    通过这五种不同类型的对象,redis可以在执行命令前,根据对象的类型来判断一个对象是否可以执行给定的命令。使用对象的另一个好处就是,我们可以针对不同的使用场景,对对象设置多种不同的数据结构实现,从而优化对象在不同使用场景下的效率。除此之外,redis对象系统还实现了基于引用计数技术的内存回收机制,当程序不再使用某个对象时,这个对象所占用的内存就会被自动释放;另外,redis还通过引用计数技术实现了对象共享机制,这一机制可以在适当的条件下,通过让多个数据库键共享同一个对象来节约内存。最后,redis的对象带有访问时间记录信息,该信息可以用于计算数据库键的空转时长,在服务器启用了maxmemory功能的情况下,空转时长较大的那些键可能会优先被服务器删除。

    redis中的每一个对象都由一个redisObject结构表示,该结构中和保存数据有关的三个属性分别是type属性、encoding属性和ptr属性。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    typedef struct redisObject{
    //类型
    unsigned type:4;
    //编码
    unsigned encoding:4;
    //指向数据的指针,如果数据为整数的话,ptr直接存储该整数的值
    void* ptr;

    ...

    } robj;

    对象的类型有以下几种:

    REDIS_STRING: 字符串对象 : "string"
    REDIS_LIST: 列表对象 : "list"
    REDIS_HASH: 哈希对象 : "hash"
    REDIS_SET: 集合对象  : "set"
    REDIS_ZSET: 有序集合对象  : "zset"
    

    在redis中,所有键的类型都是字符串。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    redis> SET msg "hello world"
    OK

    redis> TYPE msg
    string

    redis> RPUSH number 1 3 5
    OK

    redis> TYPE number

    lsit

    ...

    encoding 属性记录对象所使用的编码,也就是对象使用了什么数据结构作为底层的实现。encoding 的类型有以下几种:

    REDIS_ENCODING_INT : long 类型的整数 :"int"
    REDIS_ENCODING_EMBSTR : embstr编码的简单动态字符串 : "embstr"
    REDIS_ENCODING_RAW : 简单动态字符串 : "raw"
    REDIS_ENCODING_HT : 字典 : "hashtable"
    REDIS_ENCODING_LINKEDLIST : 双端链表 : linkedlist
    REDIS_ENCODING_ZIPLIST : 压缩列表 : "ziplist"
    REDIS_ENCODING_INTSET : 整数集合 : "inset"
    REDIS_ENCODING_SKIPLIST : 跳跃表和字典 : "skiplist"
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    redis> SET msg "hello world"
    OK

    redid> OBJECT ENCODING msg
    "embstr"

    redis> SET story "long long long ago"
    OK

    redis> OBJECT ENCODING story
    "raw"(SDS)

    redis> SADD number 1 3 5
    (integer) 3

    redis> OBJECT ENCODING number
    "inset"

    redis> SADD number "seven"
    (integer) 1

    redis> OBJECT ENCODING number
    "hashtable"

    通过encoding属性来设定对象所使用的编码,而不是为特定类型的对象关联一种固定的编码,极大的提高了redis的灵活性和效率,因为redis可以根据不同使用场景来为一个对象设置不同的编码,从而优化对象在特定场合的效率。
    例如:在列表对象包含的元素比较少的时候,redis使用压缩类表作为列表对象的底层实现:

    1. 因为压缩列表比双端列表更节约内存,而且在元素数量比较少的时候,在内存中以连续块方式保存的压缩列表比起双端列表可以更快的被载入到缓存中。
    2. 随着列表对象包含的元素越来越多,使用压缩列表来保存元素的优势逐渐消失,对象就会将底层的实现从压缩列表改为功能呢个更强大、适合保存大量元素的双端链表上面。

    有序集合对象的底层数据实现

    redis的有序集合对象zset结构使用了两种数据结构:跳表和字典。跳表来实现有序集合,可以保存执行范围类型操作的所有优点,也可以实现集合的有序,但是对于根据成员key来查找value,将会由于二分查找而时间复制度变为O(logN), 使用字典来保存所有的key-value对的话,那么既可以将查找的时间复杂度降为O(1)。为了实现高效率,有序集合就采用了跳表和字典来作为底层的数据实现。

    对象共享

    目前来说,redis会在初始化服务器时,创建一万个字符串对象,这些对象包含了从0到9999的所有整数值,当服务器需要用到这些字符串对象的时候,服务器就会使用这些共享对象,而不是创建新对象。另外,这些共享对象不仅只有字符串键可以使用,那些在数据结构中嵌套了字符对象的对象,都可以使用这邪恶共享对象。

    redis只共享包含整数值的字符串对象,而不共享包含字符串的对象,是因为对象越复杂时,共享的时候需要检查的东西越多,消耗的CPU时间就越长。如果共享独享是一个保存整数值的字符串,那么需要比较的复杂度为O(1),如果保存的是一个字符串,需要的复杂度为O(n),如果是包含多个值的对象,那么需要的时间复杂度为O(N^2)。所以为了效率的考虑,redis目前只对包含整数值的字符串对象进行共享。

    STL概览

    STL有六大组件:

    1. 容器:各种数据结构,用来存放数据,容器分为序列式容器和关联式容器。
    2. 算法:如sort、search等算法,从实现的角度来看,STL算法是一种function template。
    3. 迭代器:扮演容器和算法之间的胶合剂,是所谓的泛型指针。
    4. 仿函数:行为类似函数,可作为算法的某种策略。
    5. 适配器:一种用来修饰容器和仿函数或迭代器接口的东西,对外提供特定的接口,具有一定规则的数据操作访问。
    6. 配置器:负责空间配置与管理。

    序列式容器

    vector

    vector的安排和操作方式与array非常相似,两者的唯一区别就是空间的灵活运用。array是静态空间,一旦配置不能更改,如果需要更大的空间的话,需要重新申请内存,再负责原有的数据到新空间。而vector是动态空间,随着元素的加入,他的内部机制会自行扩充空间以容纳新元素。vector实现该机制的主要属性有size和capacity,size记录用户想要申请的空间大小或是vector实际上存储的元素的个数,而capacity记录的是vector实际申请的内存大小,capacity通常大于size,这样当需要向vector加入新元素时,就不需要每次都向系统重新申请内存,提高了数据操作的效率。

    vector有对应的迭代器来遍历自己的元素,由于当增加或删除元素时,vector内部有可能会重新分配内存空间,导致迭代器失效,所以在使用vector迭代器的时候,对vector进行元素的增加或删除操作是很危险的。

    list

    相比较vector,list在任何位置增加或删除元素时,都是常数时间的,但是在随机访问元素时,却比vector差。在STL中,list的实现时以双向环形链表的形式,只需要一个指针,就可以完整表现整个链表。STL会自己增加一个空节点,该结点连接着链表头和尾部节点,从而构成一个环。list.end()就是指向该节点。判断链表是否为空,只需要查看空节点的上一个或下一个节点是否指向自己就好。
    list有自己的迭代器,相比于vector,list的插入或删除操作都不会造成原有的迭代器失效,因为list元素的改动不会影响到其他元素的内存位置。当然,删除操作会影响到原来指向被删除的元素的迭代器。

    deque

    deque是双向开口的,可以在头部或是尾部插入或删除元素。deque与vector的最大差异,一在于deque允许常数时间内对头部进行元素的插入删除操作,二是deque没有capacity观念,因为他是动态的以分段连续的空间组合而成,随时可以增加一段新空间并连接起来。deque采用一块所谓的map作为主控,这里所谓的map是一小块连续空间,其中每个元素都是指针,指向另一段较大的连续线性空间,称为缓冲区,缓冲区才是deque的数据存储空间。

    deque也有自己的迭代器。

    适配器

    stack

    stack允许数据先进后出,STL默认采用的是deque容器来作为stack的底层实现。stack封装容器,对外提供特定的数据操作接口,只允许数据先进后出,所以stack没有迭代器。

    queue

    与stack类似,queue默认采用的也是deque来实现数据的存储,只允许数据先进先出,queue也没有迭代器。

    heap

    heap并不是STL容器组件,同时priority queue的底层实现算法。二叉堆为完全二叉树,所以可以使用数组来实现heap算法。heap分为max-heap和min-heap。由于堆也规定特定的数据操作规则,所以heap没有迭代器

    priority queue

    priority queue是一个拥有权值的queue,元素根据权值来排序。默认情况下,priority queue采用max-heap作为底层的实现,而max-heap采用vector作为底层的数据存储结构。priority queue没有自己的迭代器。

    关联式容器

    标准的STL关联式容器分为set和map两大类,以及这两大类衍生的multiset和multimap。这些都采用红黑树来实现。红黑树是STL的独立容器,不对外开放。

    set

    set的特点是所有的元素会根据元素的键值自动排序。set的元素不像map那样同时拥有key和value,set元素的key和value是一体的。 因为set的元素值就是其键值,关系到set元素的排列规则,不允许修改set的元素值。set拥有和list相同的某些性质,当客户端进行插入或删除元素时,操作之前的原有迭代器,在操作后都依然有效,当然,被删除的那个元素的迭代器必然是个例外。

    map

    map的特性是,所有元素会根据key来进行排序,map的所有元素都是pair,同时拥有key和value,因此我们可以根据map的迭代器来修改map的value。map和set一样,在删除或是插入元素时,除了别删除的元素外,其他的迭代器都是有效的。

    multiset

    multiset的性质和set一样,只不过允许有相同的值。

    multimap

    multimap的特性与map完全一样,只不过允许有相同的key。

    hashtable

    hashtable采用字典的形式来存储数据,底层实现为数组加链表。拥有自己的迭代器。

    important

    数独服务器

    采用reactor的模式,服务器有一个主线程eventloop,主要负责监听文件描述符的事件,文件描述符主要有三种,一种是以建立的连接对应的描述符,一个是服务器监听的端口,另外一个是eventfd,该文件描述符主要是用来
    唤醒阻塞在IO复用(IO采用的是epoll)上的主线程。在实现中,每一个文件描述符对应一个channel,channel对外提供四个回调函数,用来处理相应的读写或是错误以及关闭事件,该channe负责根据文件描述符的所发生的事件来执行相应的回掉函数。
    当服务器从IO复用返回的时候,会根据每一个发生事件的文件描述得到其对应的channel,让后执行channel的handevent函数。eventloop提供一个函数注册接口,外界可以通过该接口注册函数,让eventloop在执行所有的发生事件的文件描
    述符对应的处理函数后,可以执行外界所注册的函数,文件描述符监听事件的更改,删除就是通过注册函数来实现的。我们是实现了一个线程池,用来执行数独的求解,当服务器收到一个请求时,就直接把请求处理任务放到线程池里面,线程池的线程
    负责求解,并将结果返回给客户端。在整个服务过程中,所有的文件描述符的读写都有eventloop来实现,从而保证数据的完整性以及有序性。在应用层,我们实现一个缓冲类buffer,每一个连接都对应一个输入输出buffer,用来来处理所
    有的文件读写数据。当文件描述符可读时,会将数据读到buffer里面,有数据可写时,是直接写到buffer里面。由于采用的是非阻塞编程,所以需要一个buffer来实现数据的缓冲,buffer默认为40K的大小,(vector来实现,
    大大小可以自由伸缩),对于每一个输出buffer,为了防止客户端恶意发送请求,我们设置可一个阈值,但输出buffer的大小超出4M时,就默认关闭该连接。

    注意点:

    1. buffer是必须的,由于是非阻塞编程,所以必须兼顾手法数据的不平衡性,在接收端,当收到的数据不全时,需要将数据缓冲起来,在发送时,当数据不能一次性发送完时,需要将数据缓存起来。
    2. 不能多个线程同时操作一个文件描述符的IO。会造成数据的不完整。
    3. buffer开始不需要开的太大,太大会造成内存浪费,在读文件描述符的时候,为了一次性读完所有的数据,减少系统调用,可以利用栈空间,在栈中开辟一个新的空间,采用readv函数,将数据先后读到输入buffer和栈空间上,再将栈空间数据
      append到输入buffer中。

    使用C++的functor和bind机制,使得函数注册与回调可以方便的实现。

    函数回调机制:
    channel有一个handlevent,用来执行事件对应的回调。提供四个接口,类型为typedef boost::function EventCallback,来分别处理读、写、错误、关闭事件。channel由TCPConnection封装,TcpConnection由自己的
    handleRead、handleWrite、handleClose、handleError函数,用来处理相应事件,这四个函数都作为callback,注册到channel中。TcpConnection还对外提供了四个回调:
    //连接建立时调用,就是TCPConnection对象生成后,可以执行该回调。
    typedef boost::function ConnectionCallback;
    //关闭时调用
    typedef boost::function CloseCallback;
    //发送数据时,如果buffer的大小超出规定值,则调用
    typedef boost::function HighWaterMarkCallback;
    //读完数据到buffer后,调用
    typedef boost::function MessageCallback;

    在TcpConnection的handlexxx函数中,会在处理一些本身的事物后,调用对应的回调。

    TCPConnection由TCPServer产生,所以TCPServer也提供对应的回调函数,用户注册回调函数到TCPServer中,每当一个TCPConnector建立,TCPServer就会将其回调函数负责给TcpConnection对应的回调函数对象,这样就使得用户可以
    将自己想要的操作注册到TCPConnection中,从而决定事件的具体处理过程。

    股票垂直搜索

    负责从网上爬取股票新闻,我们爬取的是和讯网,对于每一篇新闻,使用股票的结构化数据计算出新闻的相关性概率,融合softmax回归模型的输出概率,通过参数拟合方法得到最终的概率序列,接着通过多标签选择算法得出新闻的
    所属股票标签,并将该新闻推送给对应的股票。

    爬取以及新闻结构化数据提取模块:

    使用python来爬取和讯网的数据,由一个模块时可以获得所有股票的最新连接的,我们开启一个线程来爬取该模块的连接,可以获得最新的新闻连接,每一个页面返回30条记录,采用redis来实现去重。程序每一秒爬取一次,为了实现数据的爬全,
    每一次爬取页面直到有连接在redis中出现重复为止。(分两个redis连接,一个是上一次的连接记录A,一个是这一次的连接记录B,如果一条连接在A中出现,那么说明已经是旧的数据了,直接终止这次爬取,如果不是,那么匹配以下B,如果重复
    直接忽略,如果不重复,那么爬取该连接,同时在B中增加一条记录)。将没有重复的连接存储在mysql里面,并记录相关的状态,比如还没爬取数据,爬取得到的状态代码等。接下来就是从mysql中查找还没有爬取的连接,爬取该连接的新闻数据。
    由于由多个线程访问数据库,所以这里需要加锁。我们利用mysql的FOR UPDATE 写锁,对每一行的数据进行写锁,从而保证线程的同步。每一个连接会有一个访问次数以及访问状态code,如果访问出错,那么次数加1,设置得到的状态code,当次数超过三次时,直接忽略该连接(statu设置为1,表示已访问)。将访问得到的新闻数据直接存在mongoDB里面。接着就是从mongoDB里面提取新闻数据,因为mongoDB存储的都是新闻的原始数据,需要进行新闻数据的结构化提取,这里又涉及到
    多线程访问mongoDB,我们利用python的线程安全的queue模块,自己建立一个中间队列,线程从队列里面获得新闻页面,如果队列的长度小于指定的阈值,那么队列自己从mongoDB里面获得数据,存在队列中,使用中间队列,使得多线程只与队列
    打交道,队列于mongoDB打交道,从而保证线程的同步。提取好新闻的结构化数据后,就直接的存在mysql中。最后就是从mysql中获得新闻,进行分类推送。也是类似于FOR UPDATE来保证线程的同步。
    爬从下载用的是pycurl模块,mongoDB用的是pymongo,使用python内建的线程安全queue, python的线程用的是内建的threading模块,类只需要继承threading.Thread类就好了,实现自己的run函数,然后生成对象,调用对象.start()
    函数就OK。

    分类模块:

    文章的特征话,获得特征向量:新对文章进行分词,利用搜狗字典以及股票的特有名词,如板块名词,股票名称等,当初的股票总数为2863个股票,需要将这些股票的名称以及所有的板块名称都录入到字典里面,并且给与最大的权限。最长的词长度
    限制在6个。采用动态规划的方法将文章进行分词,得到词向量。样本有50000多的词,进行信息熵处理后,得到最终10000个词。根据最终的词数组,将分好词的文章与数组一一对应,获得文章的特征向量。将特征向量利用softmax回归得到分类
    的股票的概率列表(所有股票的概率)。接下来是结构化分类:每一个股票都有自己的结构化数据,如名称,代码,公司,所属行业,经营范围,企业法人等,可以利用这些数据对股票进行一个分类,将股票的特征向量中的词与这些结构化信息进行匹配
    ,每一个信息都有对应的权重(根据经验值来设定),最后得到分类到该股票的权值,每一个结构化所对应的总权值是一样的,将最后的到的权值总和除以总权值,那么就是该股票的概率了。将结构化的概率和softmax概率加权求和,就得到股票的
    最后概率,将这些概率数组进行排序,从头开始选,如果有出现概率间隔比较大的,那么就截至,将以选择的作为结果输出。
    分词:采用动态规划的方法,使得分好的最终权重最大。先对文章根据停词和分割符号进行分割,得到分割段列表,对于每一个段,进行动态规划的分词,将所有段的分词合并在一起得到最终的分词结果,根据10000个特征词得到文章的最终特征向量。

    推送模块:

    采用redis缓存数据库,用户上线后,会从mysql中读取用户所关注的股票id类表,存在redis中,采用的是redis的列表键(uid, stockidlist),redis中还有另外一个数据结构,用来记录每一个股票id所拥有的新闻id列表
    (stockid, newsidlist),每一篇分好类的新闻就会往对应的stockid的新闻列表头插入该新闻的id。每一个newsidlist的长度限制为200,超过的话,直接丢尾。当用户需要获取新闻时,用户刷新一次,可以获取所有的关注股票的新闻或是
    对应股票的新闻,如果是所有股票新闻,那么直接通过(uid, stcokidlist)结构获得用户关注的股票id,然后根据(stockid, newsidlist)结构获得对应新闻id,由于一篇新闻会对应多个股票,所以这里需要先对新闻id去重,再从mysql
    里面获得所有的新闻数据(以事件排序,mysql的order by功能),将数据返回给客户端。每次最多返回前20条。第一次刷新没有时间戳,后面每次刷新都根据上一次获得的新闻的最新时间戳来获得最新的数据。如果想下拉获得更早额数据,那么需要
    客户提供上一次获得数据中的最早的时间戳,根据该时间来获得数据。获取旧数据如果每次都查找所有的newsid,在mysql中根据时间来获得合适的数据的话,会比较没效率,因为每一次都需要找出所有相关的newid,然后去掉不合适的。可以在
    redis中增加一个数据结构,记录但前所有需要推送给用户的newsid,用户第一次刷新时,直接获得所有newsid(去重),返回20条数据后,将返回的id从newsid列表中去掉,以后如果需要旧数据,那么就直接从这里的列表中获取就好了。

    对于获得特定的股票新闻,需要根据股票的id,获得newsid列表,这里获取最新和旧新闻不需要时间戳,可以根据newsid来决定那些是新的,那些是旧的,也不需要新闻id的去重。

    如果想主动推送的话,类似于发布和订阅,那么需要增加一个数据结构为(stockid, uidlist),记录每一个股票所对应的用户id列表,当有新的新闻分类到对应的股票时,那么根据股票id获得所有的用户,将新闻推给用户。这里需要注意,由于
    一篇新闻可能对应多个股票,一个用户如果恰好订阅了这多个股票的话,那么新闻会出现多次推送。这里需要将所有股票的用户搜集起来,进行去重,然后再推送。在客户端,由于收到可新的推送,可以将新闻归类到用户订阅的相应股票上,
    这样,在进行股票刷新的时候可以保证不会获得重复的数据。

    TCP Nagel算法

    1. Nagel算法

    TCP/IP协议中,无论发送多少数据,总是要在数据前面加上协议头,同时,对方接收到数据,也需要发送ACK表示确认。为了尽可能的利用网络带宽,TCP总是希望尽可能的发送足够大的数据。(一个连接会设置MSS参数,因此,TCP/IP希望每次都能够以MSS尺寸的数据块来发送数据)。Nagle算法就是为了尽可能发送大块数据,避免网络中充斥着许多小数据块。

    Nagle算法的基本定义是任意时刻,最多只能有一个未被确认的小段。 所谓“小段”,指的是小于MSS尺寸的数据块,所谓“未被确认”,是指一个数据块发送出去后,没有收到对方发送的ACK确认该数据已收到。

    Nagle算法的规则(可参考tcp_output.c文件里tcp_nagle_check函数注释):

    (1)如果包长度达到MSS,则允许发送;
    
    (2)如果该包含有FIN,则允许发送;
    
    (3)设置了TCP_NODELAY选项,则允许发送;
    
    (4)未设置TCP_CORK选项时,若所有发出去的小数据包(包长度小于MSS)均被确认,则允许发送;
    (5)上述条件都未满足,但发生了超时(一般为200ms),则立即发送。
    

    Nagle算法只允许一个未被ACK的包存在于网络,它并不管包的大小,因此它事实上就是一个扩展的停-等协议,只不过它是基于包停-等的,而不是基于字节停-等的。Nagle算法完全由TCP协议的ACK机制决定,这会带来一些问题,比如如果对端ACK回复很快的话,Nagle事实上不会拼接太多的数据包,虽然避免了网络拥塞,网络总体的利用率依然很低。

    Nagle算法是silly window syndrome(SWS)预防算法的一个半集。SWS算法预防发送少量的数据,Nagle算法是其在发送方的实现,而接收方要做的时不要通告缓冲空间的很小增长,不通知小窗口,除非缓冲区空间有显著的增长。这里显著的增长定义为完全大小的段(MSS)或增长到大于最大窗口的一半。

    注意:BSD的实现是允许在空闲链接上发送大的写操作剩下的最后的小段,也就是说,当超过1个MSS数据发送时,内核先依次发送完n个MSS的数据包,然后再发送尾部的小数据包,其间不再延时等待。(假设网络不阻塞且接收窗口足够大)

    举个例子,比如之前的blog中的实验,一开始client端调用socket的write操作将一个int型数据(称为A块)写入到网络中,由于此时连接是空闲的(也就是说还没有未被确认的小段),因此这个int型数据会被马上发送到server端,接着,client端又调用write操作写入‘\r\n’(简称B块),这个时候,A块的ACK没有返回,所以可以认为已经存在了一个未被确认的小段,所以B块没有立即被发送,一直等待A块的ACK收到(大概40ms之后),B块才被发送。

    这里还隐藏了一个问题,就是A块数据的ACK为什么40ms之后才收到?这是因为TCP/IP中不仅仅有nagle算法,还有一个TCP确认延迟机制 。当Server端收到数据之后,它并不会马上向client端发送ACK,而是会将ACK的发送延迟一段时间(假设为t),它希望在t时间内server端会向client端发送应答数据,这样ACK就能够和应答数据一起发送,就像是应答数据捎带着ACK过去。在我之前的时间中,t大概就是40ms。这就解释了为什么’\r\n’(B块)总是在A块之后40ms才发出。
    当然,TCP确认延迟40ms并不是一直不变的,TCP连接的延迟确认时间一般初始化为最小值40ms,随后根据连接的重传超时时间(RTO)、上次收到数据包与本次接收数据包的时间间隔等参数进行不断调整。另外可以通过设置TCP_QUICKACK选项来取消确认延迟。

    TCP_NODELAY 选项

    默认情况下,发送数据采用Negale 算法。这样虽然提高了网络吞吐量,但是实时性却降低了,在一些交互性很强的应用程序来说是不允许的,使用TCP_NODELAY选项可以禁止Negale 算法。

    此时,应用程序向内核递交的每个数据包都会立即发送出去。需要注意的是,虽然禁止了Negale 算法,但网络的传输仍然受到TCP确认延迟机制的影响。

    TCP_CORK 选项

    所谓的CORK就是塞子的意思,形象地理解就是用CORK将连接塞住,使得数据先不发出去,等到拔去塞子后再发出去。设置该选项后,内核会尽力把小数据包拼接成一个大的数据包(一个MTU)再发送出去,当然若一定时间后(一般为200ms,该值尚待确认),内核仍然没有组合成一个MTU时也必须发送现有的数据(不可能让数据一直等待吧)。

    然而,TCP_CORK的实现可能并不像你想象的那么完美,CORK并不会将连接完全塞住。内核其实并不知道应用层到底什么时候会发送第二批数据用于和第一批数据拼接以达到MTU的大小,因此内核会给出一个时间限制,在该时间内没有拼接成一个大包(努力接近MTU)的话,内核就会无条件发送。也就是说若应用层程序发送小包数据的间隔不够短时,TCP_CORK就没有一点作用,反而失去了数据的实时性(每个小包数据都会延时一定时间再发送)。

    Nagle算法与CORK算法区别

    Nagle算法和CORK算法非常类似,但是它们的着眼点不一样,Nagle算法主要避免网络因为太多的小包(协议头的比例非常之大)而拥塞,而CORK算法则是为了提高网络的利用率,使得总体上协议头占用的比例尽可能的小。如此看来这二者在避免发送小包上是一致的,在用户控制的层面上,Nagle算法完全不受用户socket的控制,你只能简单的设置TCP_NODELAY而禁用它,CORK算法同样也是通过设置或者清除TCP_CORK使能或者禁用之,然而Nagle算法关心的是网络拥塞问题,只要所有的ACK回来则发包,而CORK算法却可以关心内容,在前后数据包发送间隔很短的前提下(很重要,否则内核会帮你将分散的包发出),即使你是分散发送多个小数据包,你也可以通过使能CORK算法将这些内容拼接在一个包内,如果此时用Nagle算法的话,则可能做不到这一点。

    以上内容转自这里

    连续发送多份小数据时40ms延迟问题

    连续发送多份小数据时40ms延迟问题

    以及TCP_NODELAY、TCP_CORK失效问题的定位与解决

    提到TCP_NODELAY和TCP_CORK,相信很多人都很熟悉。然而由于Linux实现上的问题,这两个参数在实际使用中,并不像书里介绍的那么简单。最近DTS在解决一个TCP超时问题时,对这两个参数和它们背后所隐藏的问题有了比较深刻的认识,在此与同学们分享一下我们的经验和教训。

    问题描述

    和许多经典的分布式程序类似,DTS使用TCP长连接用于client和server的数据交互:client发送请求给server,然后等待server回应。有时候出于数据结构上的考虑,client需要先连续发送多份数据,再等待server的回应。测试发现这种情况下,server端有时会出现接收数据延迟。比如说某个case里,client会先发送275个字节,接着发送24个字节,然后再发送292字节数据等等;此时如果该TCP连接被复用过,则server端在收取24字节这批数据时会很容易出现40ms延迟。

    由于client每次发送的数据都很小,很自然想到是nagle算法延迟了client端的数据发送,于是在client端和server端都设置了TCP_NODELAY。然而测试发现,此时server虽然顺利接受了24字节数据,却在接受随后292字节数据时依然出现了40ms延迟。难道是数据太多导致TCP_NODELAY失效?因此又在client端添加了TCP_CORK选项:即如果client需要连续发送多次数据,则先关闭TCP_NODELAY,打开TCP_CORK;所有数据write完后,再关闭TCP_CORK,打开TCP_NODELAY。按照设想,client应该会把所有数据打包在一起发送,但测试结果依然和以前一样,server还是在收取第三份数据时出现了40ms的延迟。

    不得已使用tcpdump进行分析,结果如下:

    1
    2
    3
    4
    18:18:01.640134 IP jx-dp-wk11.jx.baidu.com.36989 > tc-dpf-rd-in.tc.baidu.com.licensedaemon: P 551:826(275) ack 141 win 1460 <nop,nop,timestamp 2551499424 1712127318>
    18:18:01.640151 IP jx-dp-wk11.jx.baidu.com.36989 > tc-dpf-rd-in.tc.baidu.com.licensedaemon: P 826:850(24) ack 141 win 1460 <nop,nop,timestamp 2551499424 1712127318>
    18:18:01.680812 IP tc-dpf-rd-in.tc.baidu.com.licensedaemon > jx-dp-wk11.jx.baidu.com.36989: . ack 850 win 2252 <nop,nop,timestamp 1712127359 2551499424>
    18:18:01.680818 IP jx-dp-wk11.jx.baidu.com.36989 > tc-dpf-rd-in.tc.baidu.com.licensedaemon: P 850:1142(292) ack 141 win 1460 <nop,nop,timestamp 2551499465 1712127359>

    注意红色的部分,可见client并没有将所有数据打成一个包,每次write的数据还是作为单独的包发送;此外,client在发送完24字节的数据后,一直等到server告知ack才接着发送剩下的292字节。由于server延迟了40ms才告知ack,因此导致了其接收292字节数据时也出现了40ms延迟。

    既然查出了延迟是server端delayed ack的原因,通过设置server端TCP_QUICKACK,40ms延迟的问题得到了解决。

    原因定位

    虽然DTS的延时问题暂时得到了解决,但其内在原因却使人百思不得其解:为什么TCP_NODELAY会失效?为什么TCP_CORK无作为?…… 在STL同学的帮助下,我们逐渐对这些困惑有了答案。

    首先介绍下delayed ack算法:当协议栈接受到TCP数据时,并不一定会立刻发送ACK响应,而是倾向于等待一个超时或者满足特殊条件时再发送。对于Linux实现,这些特殊条件如下:

    1)收到的数据已经超过了full frame size

    2)或者处于快速回复模式

    3)或者出现了乱序的包

    4)或者接收窗口的数据足够多

    如果接收方有数据回写,则ACK也会搭车一起发送。当以上条件都不满足时,接收方会延迟40ms再回应ACK。

    1.为什么TCP_NODELAY失效

    UNIX网络编程这本书介绍说,TCP_NODELAY同时禁止了nagle算法和delayed ACK算法,因此小块数据可以直接发送。然而Linux实现中,TCP_NODELAY只禁止了nagle算法。另一方面,协议栈在发送包的时候,不仅受到TCP_NODELAY的影响,还受到协议栈里面拥塞窗口的影响。由于server端delayed ack,client迟迟无法收到ack应答,拥塞窗口堵满,从而无法继续发送更多数据;一直到40ms后ack达到,才能继续发送(题外话: TCP_NODELAY在FREEBSD上性能优于Linux上,因为FREEBSD并不像Linux一样需要第一个包到达后就响应ACK)。

    这也解释了为什么延时现象在重用过的TCP连接上特别容易出现:目前使用的52bs内核中,连接刚建立时拥塞窗口默认是3,因此可以发送3个数据包,而后拥塞窗口变为2,就会导致第3个292字节的包发不出去。

    2.为什么TCP_CORK失效

    TCP_CORK会将发送端多份数据打成一个包,待到TCP_CORK关闭后一起发送。Linux Man手册上也描述了TCP_CORK选项和TCP_NODELAY一起使用的情形。然而根据之前tcpdump的结果,client端设置TCP_CORK后并没有发挥效果。继续测试发现,只要设置过TCP_NODELAY选项,即使随后关闭也会导致TCP_CORK无效;如果从未设置过TCP_NODELAY,则TCP_CORK可以产生效果。

    根据STL同学对协议栈代码的调研,发现这个是Linux实现上的问题。在内核中,设置启动TCP_NODELAY选项后,内核会为socket增加两个标志位TCP_NAGLE_OFF和TCP_NAGLE_PUSH,关闭TCP_NODELAY的时候,内核只去掉了TCP_NAGLE_OFF标志位。而在发包的时候判断的却恰恰是TCP_NAGLE_PUSH标志位,如果该位置位设置,就直接把包发出去,从而导致TCP_CORK发挥不了作用。这很可能是这一版本Linux内核实现上的bug。

    3.TCP_QUICKACK的作用和限制

    前面介绍delayed ack算法时,讲到协议栈迅速回复ack的情形之一就是进入到快速回复模式。而TCP_QUICKACK选项就是向内核建议进入快速回复模式。快速回复ack模式的判断条件如下:(tp->ack.quick && tp->ack.pingpong),其中设置QUICKACK选项会置pingpong=0。

    然而,随着TCP连接的重用和数据的不断收发,快速回复模式有可能失效。例如在后续的交互过程当中,pingpong变为1的条件就有:1.收到fin后;2. 发送方发送数据时,发现当前时间与上次接收数据的时间小于40ms。此外,发送方发现数据包带有ack标志位时,也会减小ack.quick值。这些都会导致快速回复模式的退出。因此,即使每次接受数据前都设置TCP_QUICKACK选项,也不能完全解决delayed ack问题。

    解决方案

    经过上述的测试与分析,可以认识到当连续发送多个小数据时,TCP_NODELAY作用并不明显,TCP_CORK无法像宣传的那样和TCP_NODELAY混合使用,而TCP_QUICKACK也不能完全解决问题。因此,我们最终的解决方案如下:

    (1)在client端多次发送数据时,先打开TCP_CORK选项,发送完后再关闭TCP_CORK,将多份小数据打成一个包发送;此外,client端不能设置TCP_NODELAY选项,以避免TCP_CORK失效。

    (2)server端开启TCP_QUICKACK选项,尽量快速回复ack。

    通过这个延时问题的解决,可以看到由于Linux实现策略上的问题,TCP_NODELAY和TCP_CORK还是暗藏了不少陷阱。实际应用中,其实也可以绕过这些参数,在应用层将多份数据序列化到一个buffer中,或者使用writev系列函数。然而,这些方法需要额外的内存拷贝,或者让传输对象对外暴露过多的数据结构信息,并不一定容易实现,也会添加代码重构的代价。

    另一方面,考虑到那些使用TCP进行异步请求的应用,由于多个请求需要同时复用一个TCP连接,也很容易出现延时问题;而无论是通过TCP_CORK还是writev哪种方法,都不太适合这种异步场景。最近STL推出的新内核添加了一个禁止delayed ack的系统参数,使用该参数理论上讲可以彻底根除40ms的延迟问题。

    以上内容转自这里

    ICMP 协议

    ICMP 经常被认为是 IP 层的一个组成部分,它传递差错报文以及其他需要注意的信息。ICMP 报文通常被 IP 层或更高层协议(TCP 或 UDP)使用。ICMP 报文是在 IP 数据报内部传输的。IP 协议是不可靠协议,不能保证 IP 数据报能够成功的到达目的主机,无法进行差错控制,而 ICMP 协议能够协助 IP 协议完成这些功能。下面是 ICMP 报文的数据结构:

    类型:一个 8 位类型字段,表示 ICMP 数据包类型;

    代码:一个 8 位代码域,表示指定类型中的一个功能,如果一个类型中只有一种功能,代码域置为 0;

    检验和:数据包中 ICMP 部分上的一个 16 位检验和;

    以下针对 ICMP 差错报文的类型进行分析:

    1、ICMP 目标不可达消息:IP 路由器无法将 IP 数据报发送给目的地址时,会给发送端主机返回一个目标不可达 ICMP 消息,并在这个消息中显示不可达的具体原因。

    2、ICMP 重定向消息:如果路由器发现发送端主机使用次优的路径发送数据时,那么它会返回一个 ICMP 重定向消息给这个主机,这个消息包含了最合适的路由信息和源数据。主要发生在路由器持有更好的路由信息的情况下,路由器会通过这个 ICMP 重定向消息给发送端主机一个更合适的发送路由。

    3、ICMP 超时消息:IP 数据包中有一个字段 TTL(Time to live,生存周期),它的值随着每经过一个路由器就会减 1,直到减到 0 时该 IP 数据包被丢弃。此时,IP 路由器将发送一个 ICMP 超时消息给发送端主机,并通知该包已被丢弃。

    4、源抑制消息:当 TCP/IP 主机发送数据到另一主机时,如果速度达到路由器或者链路的饱和状态,路由器发出一个 ICMP 源抑制消息。

    ICMP 查询报文

    ICMP 回送消息:用于进行通信的主机或路由之间,判断发送数据包是否成功到达对端的消息。可以向对端主机发送回送请求消息,也可以接收对端主机回来的回送应答消息。

    ICMP 地址掩码消息:主要用于主机或路由想要了解子网掩码的情况。可以向那些主机或路由器发送 ICMP 地址掩码请求消息,然后通过接收 ICMP 地址掩码应答消息获取子网掩码信息。

    ICMP 时间戳消息:可以向那些主机或路由器发送 ICMP 时间戳请求消息,然后通过接收 ICMP 时间戳应答消息获取时间信息。

    Ping 程序

    Ping 程序利用 ICMP 回显请求报文和回显应答报文(而不用经过传输层)来测试目标主机是否可达。它是一个检查系统连接性的基本诊断工具。

    ICMP 回显请求和 ICMP 回显应答报文是配合工作的。当源主机向目标主机发送了 ICMP 回显请求数据包后,它期待着目标主机的回答。目标主机在收到一个 ICMP 回显请求数据包后,它会交换源、目的主机的地址,然后将收到的 ICMP 回显请求数据包中的数据部分原封不动地封装在自己的 ICMP 回显应答数据包中,然后发回给发送 ICMP 回显请求的一方。如果校验正确,发送者便认为目标主机的回显服务正常,也即物理连接畅通。

    Traceroute 程序

    Traceroute 程序主要用来侦测源主机到目的主机之间所经过的路由的情况。

    Traceroute 使用 ICMP 报文和 IP 首部中的 TTL 字段,它充分利用了 ICMP 超时消息。其原理很简单,开始时发送一个 TTL 字段为 1 的 UDP 数据报,而后每次收到 ICMP 超时萧后,按顺序再发送一个 TTL 字段加 1 的 UDP 数据报,以确定路径中的每个路由器,而每个路由器在丢弃 UDP 数据报时都会返回一个 ICMP 超时报文,而最终到达目的主机后,由于 ICM P选择了一个不可能的值作为 UDP 端口(大于30000)。这样目的主机就会发送一个端口不可达的 ICMP 差错报文。

    DDOS攻击简介

    DDOs攻击主要有两种类型:流量攻击和占用服务器资源攻击

    针对于TCP/IP协议的不同,ddos可以利用好几个协议的漏洞进行攻击。

    基于TCP协议的攻击

    SYN Flood 攻击

    基于TCP协议的攻击主要利用的是TCP的三次握手漏洞,由于TCP建立连接的时候需要三次握手,当服务器收到一个SYN包后,服务器会处于SYN_Received状态,并且在系统中保存半连接的数据,同时发送SYN-ACK包给客户端,但是此时客户端消失了,而处于这个状态的服务器会有一个定时器,在收不到客户端的回复的时候,会重新的发送SYN-ACk数据包(3-5次,并且等待一个SYN-time,一般是30秒到2分钟),此时半连接的数据一直占着系统资源,这样的话,如果有大量这种行为就会导致服务器资源被一直占用着,而正常连接行为的客户却得不到处理。这就是所谓的SYN攻击

    ACK Flood 攻击

    ACK Flood攻击是在TCP连接建立之后,所有的数据传输TCP报文都是带有ACK标志位的,主机在接收到一个带有ACK标志位的数据包的时候,需要检查该数据包所表示的连接四元组是否存在,如果存在则检查该数据包所表示的状态是否合法,然后再向应用层传递该数据包。如果在检查中发现该数据包不合法,例如该数据包所指向的目的端口在本机并未开放,则主机操作系统协议栈会回应RST包告诉对方此端口不存在。
    这里,服务器要做两个动作:查表、回应 ACK/RST。这种攻击方式显然没有SYN Flood给服务器带来的冲击大,因此攻击者一定要用大流量ACK小包冲击才会对服务器造成影响。按照我们对TCP协议的理解,随机源IP的ACK小包应该会被Server很快丢弃,因为在服务器的TCP堆栈中没有这些ACK包的状态信息。但是实际上通过测试,发现有一些TCP服务会对ACK Flood比较敏感,比如说JSP Server,在数量并不多的ACK小包的打击下,JSP Server就很难处理正常的连接请求。对于Apache或者IIS来说,10kpps的ACK Flood不构成危胁,但是更高数量的ACK Flood会造成服务器网卡中断频率过高,负载过重而停止响应。可以肯定的是,ACK Flood不但可以危害路由器等网络设备,而且对服务器上的应用有不小的影响。

    也有另外一种攻击:connection 攻击,就是大量的肉机与服务器建立连接,占用服务器的资源不放,而一台服务器的连接数量是有限的,大量的连接都被占用了,新的正常连接得不到服务。

    基于UDP的攻击

    UDP Flood是日渐猖厥的流量型DoS攻击,原理也很简单。常见的情况是利用大量UDP小包冲击DNS服务器或Radius认证服务器、流媒体视频服务器。 100k pps的UDP Flood经常将线路上的骨干设备例如防火墙打瘫,造成整个网段的瘫痪。由于UDP协议是一种无连接的服务,在UDP FLOOD攻击中,攻击者可发送大量伪造源IP地址的小UDP包。但是,由于UDP协议是无连接性的,所以只要开了一个UDP的端口提供相关服务的话,那么就可针对相关的服务进行攻击。

    基于ICMP的攻击

    死亡之ping

    IP协议规定IP包最大尺寸为65536,大部分的处理程序在处理IP包的时候,会假定报文不会超过最大的尺寸,利用该漏洞,可以发送大于65536的数据包,使得系统在处理报文的时候发生内存溢出,从而使得系统崩溃。这就是死亡之ping的实现原理,发送大于限制长度的报文来使得服务器出现系统奔溃

    echo攻击

    利用ICMP的echo机制,对服务器发送大量的ICMP包,占用服务器的带宽,也可以利用ICMP的广播机制,将源端IP伪造成服务器的IP,向网络广播ICMP echo request,从而使得大量的网络机器向服务器发送ICMP echo应答包,占用服务器的大量带宽。

    针对链接的ICMP DoS

    针对连接的DoS攻击,可以终止现有的网络连接。针对网络连接的DoS攻击会影响所有的IP设备,因为它使用了合法的ICMP消息。Nuke通过发送一个伪造的ICMP Destination Unreachable或Redirect消息来终止合法的网络连接。更具恶意的攻击,如puke和smack,会给某一个范围内的端口发送大量的数据包,毁掉大量的网络连接,同时还会消耗受害主机CPU的时钟周期。

    基于ICMP重定向的路由欺骗技术

    攻击者可利用ICMP重定向报文破坏路由,并以此增强其窃听能力。除了路由器,主机必须服从ICMP重定向。如果一台机器想网络中的另一台机器发送了一个ICMP重定向消息,这就可能引起其他机器具有一张无效的路由表。如果一台机器伪装成路由器截获所有到某些目标网络或全部目标网络的IP数据包,这样就形成了窃听。通过ICMP技术还可以抵达防火墙后的机器进行攻击和窃听。

    注:重定向路由欺骗技术尚无实际应用。

    HTTP Get 攻击

    这种攻击主要是针对存在ASP、JSP、PHP、CGI等脚本程序,并调用MSSQLServer、MySQLServer、Oracle等数据库的网站系统而设计的,特征是和服务器建立正常的TCP连接,并不断的向脚本程序提交查询、列表等大量耗费数据库资源的调用,典型的以小博大的攻击方法。一般来说,提交一个GET或POST指令对客户端的耗费和带宽的占用是几乎可以忽略的,而服务器为处理此请求却可能要从上万条记录中去查出某个记录,这种处理过程对资源的耗费是很大的,常见的数据库服务器很少能支持数百个查询指令同时执行,而这对于客户端来说却是轻而易举的,因此攻击者只需通过Proxy代理向主机服务器大量递交查询指令,只需数分钟就会把服务器资源消耗掉而导致拒绝服务,常见的现象就是网站慢如蜗牛、ASP程序失效、PHP连接数据库失败、数据库主程序占用CPU偏高。

    内网穿透简介

    NAT的类型

    NAT一般有两种大的类型:cone(漏斗)类型和symmetric(对称)类型

    cone

    cone类型又分为三种小的类型,分别为:full cone、restricted cone和port restricted cone

    在cone类型中,内网ip-port对在NAT中的映射是不变的,即使所访问的外网ip-port端口不一样,这也是其命名为cone的原因。

    full cone

    full cone是限制最少的类型,在NAT服务器上,一个内网的ip-port对只映射到一个公网的ip-port对,而不管内网主机访问的外网ip-port对的不同。

    restricted cone

    restricted cone相对于full cone类型增加了外网ip的限制,只有内网的机器访问了某个外网的机器,该外网机器的数据包才可以通过NAT的限制。在NAT的映射中,NAT维持一个映射关系:
    {对端外网ip:内网ip:内网port}—->{公网ip:公网port},同一个外网ip的机器不同的port都可以通过该映射,从而实现与内网的通讯。

    port restricted cone

    port restricted cone类型在restricted cone类型的基础上增加了port的限制,必须又内网的机器访问外网机器的某个端口,该外网机器的对应端口发出的数据包才可以通过NAT。NAT维持的映射关系如下:
    {对端外网ip:对端外网port:内网ip:内网port}—->{公网ip:公网port},同一个机器的特定端口才可以通过该映射。

    注意,在以上的三种类型中,只要内网ip-port对不变,其在NAT的映射所对应的公网ip-port对就不会改变,也就是说,同一个内网ip-port在通讯过程中,其所对外的公网ip-port是不变的

    symmetric

    该类型的限制最严格,端口对端口的通讯都会有一个特定的映射,就拿port restricted cone来说,对于映射:{对端外网ip:对端外网port:内网ip:内网port}—->{公网ip:公网port}, 只要是四元组中的内网ip:内网port不变,那么所有的四元组都会被映射到同一个{公网ip:公网port},而在symmetric类型下,只要四元组中有一个是变化的,都会被映射到不同的而且唯一的{公网ip:公网port}。

    cone 的映射是多对一的映射,类似于一个漏斗,而symmetric的映射是一个一对一的映射,也就是对称的映射。

    NAT穿透

    根据NAT的类型不同,具体的实现细节也是不同的。但是不管是NAT类型不同,还是所采用的打洞技术不同,都需要使用到第三方服务器作为彼此信息的转发者。采用UDP来实现打洞,成功率会比较高,实现起来也比较容易。而采用TCP来打洞的话,成功率较低,而且实现起来会比较的麻烦,主要的原因就是对于TCP来说,既要打洞又要建立监听,而两者所使用的端口都是一样的,而且如果两端同时收到对端的SYN,那么两端都会处于SYN-Received状态,具体进一步的处理就要看不同的系统的TCP栈处理程序了,这个也就会导致TCP在打洞方面的成功率低于UDP。

    typedef与define的区别

    typedef

    typedef故名思意就是类型定义的意思,但是它并不是定义一个新的类型而是给已有的类型起一个别名,在这一点上与引用的含义类似,引用是变量或对象的别名,而typedef定义的是类型的别名。typedef的作用主要有两点:

    1.1 简化复杂的类型声明

    简化复杂的类型声明,或给已有类型起一含义明确的别名;如:

    1
    2
    3
    4
    5
    //声明了一个返回 bool 类型并带有两个(int和double)形参的函数的指针类型FuncPointer
    typedef bool (*FuncPointer)(int, double);

    //声明了一个FuncPointer类型的函数指针对象pFunc
    FuncPointer pFunc;

    1.2 定义与平台无关的类型

    定义与平台无关的类型,屏蔽不同平台的类型差异化;如:
    用typedef来定义与平台无关的类型。
    比如定义一个叫 REAL 的浮点类型,在目标平台一上,让它表示最高精度的类型为:
    typedef long double REAL;
    在不支持 long double 的平台二上,改为:
    typedef double REAL;
    在连 double 都不支持的平台三上,改为:
    typedef float REAL;
    也就是说,当跨平台时,只要改下 typedef 本身就行,不用对其他源码做任何修改。
    标准库就广泛使用了这个技巧,比如size_t。另外,因为typedef是定义了一种类型的新别名,不是简单的字符串替换,所以它比宏来得稳健。

    1.3 与struct的结合使用

    在C++中,struct与class的作用相同,就是默认的访问权限不同,struct默认为public,而class默认为private的。

    1
    2
    3
    4
    5
    6
    7
    struct Person  
    {
    string name;
    int age;
    float height;
    };
    Person person;

    定义一个Struct的类型Person,定义一个Person的对象person。

    1
    2
    3
    4
    5
    6
    struct Person  
    {
    string name;
    int age;
    float height;
    }person;

    定义一个Struct的类型Person,在定义的同时还声明了一个Person的对象person。

    但是在C语言中,struct的定义和声明要用typedef。

    1
    2
    3
    4
    5
    6
    7
    typedef struct __Person  
    {
    string name;
    int age;
    float height;
    }Person; //这是Person是结构体的一个别名
    Person person;

    如果没有typedef就必须用struct Person person;来声明,如:

    1
    2
    3
    4
    5
    6
    7
    struct Person  
    {
    string name;
    int age;
    float height;
    };
    struct Person person;


    1
    2
    3
    4
    5
    6
    struct Person  
    {
    string name;
    int age;
    float height;
    }person; //person是Person的对象

    typedef与#define的区别

    2.1. 执行时间不同

    关键字typedef在编译阶段有效,由于是在编译阶段,因此typedef有类型检查的功能。

    #define则是宏定义,发生在预处理阶段,也就是编译之前,它只进行简单而机械的字符串替换,而不进行任何检查。
    typedef会做相应的类型检查:

    1
    2
    3
    4
    5
    6
    typedef unsigned int UINT;  
    void func()
    {

    UINT value = "abc"; // error C2440: 'initializing' : cannot convert from 'const char [4]' to 'UINT'
    cout << value << endl;
    }

    #define不做类型检查:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    //#define用法例子:  
    #define f(x) x*x
    int main()
    {

    int a=6, b=2, c;
    c=f(a) / f(b);
    printf("%d\n", c);
    return 0;
    }

    程序的输出结果是: 36,根本原因就在于#define只是简单的字符串替换。

    2.2. 功能有差异

    typedef用来定义类型的别名,定义与平台无关的数据类型,与struct的结合使用等。

    #define不只是可以为类型取别名,还可以定义常量、变量、编译开关等。

    2.3.作用域不同

    #define没有作用域的限制,只要是之前预定义过的宏,在以后的程序中都可以使用。
    而typedef有自己的作用域。

    【例2.3.1】没有作用域的限制,只要是之前预定义过就可以

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    void func1()  
    {

    #define HW "HelloWorld";
    }

    void func2()
    {

    string str = HW;
    cout << str << endl;
    }

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    【例2.3.2】而typedef有自己的作用域

    void func1()
    {

    typedef unsigned int UINT;
    }

    void func2()
    {

    UINT uValue = 5;//error C2065: 'UINT' : undeclared identifier
    }

    【例2.3.3】

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    class A  
    {

    typedef unsigned int UINT;
    UINT valueA;
    A() : valueA(0){}
    };

    class B
    {

    UINT valueB;
    //error C2146: syntax error : missing ';' before identifier 'valueB'
    //error C4430: missing type specifier - int assumed. Note: C++ does not support default-int
    };

    上面例子在B类中使用UINT会出错,因为UINT只在类A的作用域中。此外,在类中用typedef定义的类型别名还具有相应的访问权限

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    class A  
    {
    typedef unsigned int UINT;
    UINT valueA;
    A() : valueA(0){}
    };

    void func3()
    {

    A::UINT i = 1;
    // error C2248: 'A::UINT' : cannot access private typedef declared in class 'A'
    }

    而给UINT加上public访问权限后,则可编译通过。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    class A  
    {
    public:
    typedef unsigned int UINT;
    UINT valueA;
    A() : valueA(0){}
    };

    void func3()
    {

    A::UINT i = 1;
    cout << i << endl;
    }

    2.4. 对指针的操作

    二者修饰指针类型时,作用不同。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    typedef int * pint;  
    #define PINT int *

    int i1 = 1, i2 = 2;

    const pint p1 = &i1; //p不可更改,p指向的内容可以更改,相当于 int * const p;
    const PINT p2 = &i2; //p可以更改,p指向的内容不能更改,相当于 const int *p;或 int const *p;

    pint s1, s2; //s1和s2都是int型指针
    PINT s3, s4; //相当于int * s3,s4;只有一个是指针。

    void TestPointer()
    {

    cout << "p1:" << p1 << " *p1:" << *p1 << endl;
    //p1 = &i2; //error C3892: 'p1' : you cannot assign to a variable that is const
    *p1 = 5;
    cout << "p1:" << p1 << " *p1:" << *p1 << endl;

    cout << "p2:" << p2 << " *p2:" << *p2 << endl;
    //*p2 = 10; //error C3892: 'p2' : you cannot assign to a variable that is const
    p2 = &i1;
    cout << "p2:" << p2 << " *p2:" << *p2 << endl;
    }

    结果:
    p1:00EFD094 *p1:1
    p1:00EFD094 *p1:5
    p2:00EFD098 *p2:2
    p2:00EFD094 *p2:5

    以上内容转自这里

    epoll之ET和LT编程

    在一个非阻塞的socket上调用read/write函数, 返回EAGAIN或者EWOULDBLOCK(注: EAGAIN就是EWOULDBLOCK)
    从字面上看, 意思是:EAGAIN: 再试一次,EWOULDBLOCK: 如果这是一个阻塞socket, 操作将被block,perror输出: Resource temporarily unavailable

    总结:
    这个错误表示资源暂时不够,能read时,读缓冲区没有数据,或者write时,写缓冲区满了。遇到这种情况,如果是阻塞socket,read/write就要阻塞掉。而如果是非阻塞socket,read/write立即返回-1, 同时errno设置为EAGAIN。
    所以,对于阻塞socket,read/write返回-1代表网络出错了。但对于非阻塞socket,read/write返回-1不一定网络真的出错了。可能是Resource temporarily unavailable。这时你应该再试,直到Resource available。

    综上,对于non-blocking的socket,正确的读写操作为:
    读:忽略掉errno = EAGAIN的错误,下次继续读
    写:忽略掉errno = EAGAIN的错误,下次继续写

    对于select和epoll的LT模式,这种读写方式是没有问题的。但对于epoll的ET模式,这种方式还有漏洞。

    epoll的两种模式LT和ET
    二者的差异在于level-trigger模式下只要某个socket处于readable/writable状态,无论什么时候进行epoll_wait都会返回该socket;而edge-trigger模式下只有某个socket从unreadable变为readable或从unwritable变为writable时,epoll_wait才会返回该socket。

    所以,在epoll的ET模式下,正确的读写方式为:
    读:只要可读,就一直读,直到返回0,或者 errno = EAGAIN
    写:只要可写,就一直写,直到数据发送完,或者 errno = EAGAIN

    正确的读

    1
    2
    3
    4
    5
    6
    7
    n = 0;
    while ((nread = read(fd, buf + n, BUFSIZ-1)) > 0) {
    n += nread;
    }
    if (nread == -1 && errno != EAGAIN) {
    perror("read error");
    }

    正确的写

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    int nwrite, data_size = strlen(buf);
    n = data_size;
    while (n > 0) {
    nwrite = write(fd, buf + data_size - n, n);
    if (nwrite < n) {
    if (nwrite == -1 && errno != EAGAIN) {
    perror("write error");
    }
    break;
    }
    n -= nwrite;
    }

    正确的accept,accept 要考虑 2 个问题

    (1) 阻塞模式 accept 存在的问题

    考虑这种情况:TCP连接被客户端夭折,即在服务器调用accept之前,客户端主动发送RST终止连接,导致刚刚建立的连接从就绪队列中移出,如果套接口被设置成阻塞模式,服务器就会一直阻塞在accept调用上,直到其他某个客户建立一个新的连接为止。但是在此期间,服务器单纯地阻塞在accept调用上,就绪队列中的其他描述符都得不到处理。

    解决办法是把监听套接口设置为非阻塞,当客户在服务器调用accept之前中止某个连接时,accept调用可以立即返回-1,这时源自Berkeley的实现会在内核中处理该事件,并不会将该事件通知给epool,而其他实现把errno设置为ECONNABORTED或者EPROTO错误,我们应该忽略这两个错误。

    (2)ET模式下accept存在的问题

    考虑这种情况:多个连接同时到达,服务器的TCP就绪队列瞬间积累多个就绪连接,由于是边缘触发模式,epoll只会通知一次,accept只处理一个连接,导致TCP就绪队列中剩下的连接都得不到处理。

    解决办法是用while循环抱住accept调用,处理完TCP就绪队列中的所有连接后再退出循环。如何知道是否处理完就绪队列中的所有连接呢?accept返回-1并且errno设置为EAGAIN就表示所有连接都处理完。

    综合以上两种情况,服务器应该使用非阻塞地accept,accept在ET模式下的正确使用方式为:

    1
    2
    3
    4
    5
    6
    7
    while ((conn_sock = accept(listenfd,(struct sockaddr *) &remote, (size_t *)&addrlen)) > 0) {
    handle_client(conn_sock);
    }
    if (conn_sock == -1) {
    if (errno != EAGAIN && errno != ECONNABORTED && errno != EPROTO && errno != EINTR)
    perror("accept");
    }

    epoll ET 模式简单HTTP服务器代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    #include <sys/socket.h>
    #include <sys/wait.h>
    #include <netinet/in.h>
    #include <netinet/tcp.h>
    #include <sys/epoll.h>
    #include <sys/sendfile.h>
    #include <sys/stat.h>
    #include <unistd.h>
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <strings.h>
    #include <fcntl.h>
    #include <errno.h>

    #define MAX_EVENTS 10
    #define PORT 8080

    //设置socket连接为非阻塞模式
    void setnonblocking(int sockfd) {
    int opts;

    opts = fcntl(sockfd, F_GETFL);
    if(opts < 0) {
    perror("fcntl(F_GETFL)\n");
    exit(1);
    }
    opts = (opts | O_NONBLOCK);
    if(fcntl(sockfd, F_SETFL, opts) < 0) {
    perror("fcntl(F_SETFL)\n");
    exit(1);
    }
    }

    int main(){
    struct epoll_event ev, events[MAX_EVENTS];
    int addrlen, listenfd, conn_sock, nfds, epfd, fd, i, nread, n;
    struct sockaddr_in local, remote;
    char buf[BUFSIZ];

    //创建listen socket
    if( (listenfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
    perror("sockfd\n");
    exit(1);
    }
    setnonblocking(listenfd);
    bzero(&local, sizeof(local));
    local.sin_family = AF_INET;
    local.sin_addr.s_addr = htonl(INADDR_ANY);;
    local.sin_port = htons(PORT);
    if( bind(listenfd, (struct sockaddr *) &local, sizeof(local)) < 0) {
    perror("bind\n");
    exit(1);
    }
    listen(listenfd, 20);

    epfd = epoll_create(MAX_EVENTS);
    if (epfd == -1) {
    perror("epoll_create");
    exit(EXIT_FAILURE);
    }

    ev.events = EPOLLIN;
    ev.data.fd = listenfd;
    if (epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &ev) == -1) {
    perror("epoll_ctl: listen_sock");
    exit(EXIT_FAILURE);
    }

    for (;;) {
    nfds = epoll_wait(epfd, events, MAX_EVENTS, -1);
    if (nfds == -1) {
    perror("epoll_pwait");
    exit(EXIT_FAILURE);
    }

    for (i = 0; i < nfds; ++i) {
    fd = events[i].data.fd;
    if (fd == listenfd) {
    while ((conn_sock = accept(listenfd,(struct sockaddr *) &remote,
    (size_t *)&addrlen)) > 0) {
    setnonblocking(conn_sock);
    ev.events = EPOLLIN | EPOLLET;
    ev.data.fd = conn_sock;
    if (epoll_ctl(epfd, EPOLL_CTL_ADD, conn_sock,
    &ev) == -1) {
    perror("epoll_ctl: add");
    exit(EXIT_FAILURE);
    }
    }
    if (conn_sock == -1) {
    if (errno != EAGAIN && errno != ECONNABORTED
    && errno != EPROTO && errno != EINTR)
    perror("accept");
    }
    continue;
    }
    if (events[i].events & EPOLLIN) {
    n = 0;
    while ((nread = read(fd, buf + n, BUFSIZ-1)) > 0) {
    n += nread;
    }
    if (nread == -1 && errno != EAGAIN) {
    perror("read error");
    }
    ev.data.fd = fd;
    ev.events = events[i].events | EPOLLOUT;
    if (epoll_ctl(epfd, EPOLL_CTL_MOD, fd, &ev) == -1) {
    perror("epoll_ctl: mod");
    }
    }
    if (events[i].events & EPOLLOUT) {
    sprintf(buf, "HTTP/1.1 200 OK\r\nContent-Length: %d\r\n\r\nHello World", 11);
    int nwrite, data_size = strlen(buf);
    n = data_size;
    while (n > 0) {
    nwrite = write(fd, buf + data_size - n, n);
    if (nwrite < n) {
    if (nwrite == -1 && errno != EAGAIN) {
    perror("write error");
    }
    break;
    }
    n -= nwrite;
    }
    close(fd);
    }
    }
    }

    return 0;
    }

    以上内容转自这里

    socket非阻塞读写

    读操作

    对于阻塞的socket,当socket的接收缓冲区中没有数据时,read调用会一直阻塞住,直到有数据到来才返回。当socket缓冲区中的数据量小于期望读取的数据量时,返回实际读取的字节数。当sockt的接收缓冲区中的数据大于期望读取的字节数时,读取期望读取的字节数,返回实际读取的长度。对于非阻塞socket而言,socket的接收缓冲区中有没有数据,read调用都会立刻返回。接收缓冲区中有数据时,与阻塞socket有数据的情况是一样的,如果接收缓冲区中没有数据,则返回-1,错误号为EWOULDBLOCK或EAGAIN,表示该操作本来应该阻塞的,但是由于本socket为非阻塞的socket,因此立刻返回,遇到这样的情况,可以在下次接着去尝试读取。如果返回值是其它负值,则表明读取错误。因此,非阻塞的read调用一般这样写:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    if(read(sock_fd, buffer, len) < 0)
    {
    if(errno == EWOULDBLOCK || errno == EAGAIN)
    {
    //没有读到数据

    }
    else
    {
    //读取失败

    }
    }
    else
    {
    //读到数据

    }

    写操作

    对于写操作write,原理是类似的,非阻塞socket在发送缓冲区没有空间时会直接返回-1,错误号EWOULDBLOCK或EAGA,表示没有空间可写数据,如果错误号是别的值,则表明发送失败。如果发送缓冲区中有足够空间或者是不足以拷贝所有待发送数据的空间的话,则拷贝前面N个能够容纳的数据,返回实际拷贝的字节数。而对于阻塞Socket而言,如果发送缓冲区没有空间或者空间不足的话,write操作会直接阻塞住,如果有足够空间,则拷贝所有数据到发送缓冲区,然后返回.非阻塞的write操作一般写法是:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    int write_pos = 0;
    int nLeft = nLen;
    while(nLeft > 0)
    {
    int nWrite = 0;
    if( (nWrite = write(sock_fd, data+write_pos, nLeft)) <= 0)
    {
    if(errno == EWOULDBLOCK || errno == EAGAIN)
    {
    nWrite = 0;
    }
    else
    {
    //写失败
    }
    }
    else
    {
    nLeft -= nWrite;
    wirte_pos += nWrite;
    }
    }

    以上内容转自这里

    C++之auto、static、extern、register

    extern

    extern 用来声明一个外部变量或是函数,表示该变量已经在其他的地方定义了,这里只是做一个引用而已,不会产生新的变量。对于extern修饰的变量,编译器会在所在的文件先看看有没有对该变量的定义,有的话,直接应用,没有的话再到其他的文件里面进行查找。由于变量已经在其他的地方定义了,所以extern int a = 10;这种写法是不对的,会造成重定义错误!

    static

    static可以用来修饰变量和函数。

    修饰变量

    static修饰的变量都存在静态数据区里面,由于静态数据区内存都是被系统初始化为0的,所以static修饰的变量默认初始值为0。
    static修饰的变量的作用域只限制于该变量所在的文件,外面的文件不可以使用该变量,所以多个文件可以有相同的变量名(需要使用static修饰),只要在编译的最后链接阶段,在全局情况下,不出现相同的变量名就好。

    修饰函数

    static修饰的函数主要有两个作用:限定该函数的作用域为本文件可见以及static函数在内存中只有一份,普通函数在每个被调用中维持一份拷贝,因此对于static函数的调用会比普通函数快很多。

    类中的static

    在类中,static修饰的成员变量或是函数表明该变量或是函数是属于类的,所以没有对应的this指针。static修饰的函数只能访问类的static修饰的成员变量,并且static修饰的函数不能是虚函数。

    auto

    普通局部栈变量,是自动存储,这种对象会自动创建和销毁 ,建议这个变量要放在堆栈上面,调用函数时分配内存,函数结束时释放内存。一般隐藏auto默认为自动存储类别。我们程序都变量大多是自动变量。auto不能修饰全局变量,因为该变量的内存是放在栈上面的,有系统自动创建和销毁,所以不能用来修饰全局变量。在函数里面,一般所声明的变量都是直接的默认为auto的。

    register

    寄存器变量,请求编译器将这个变量保存在CPU的寄存器中,从而加快程序的运行。系统的寄存器是有限制的,声明变量时如:register int i.这种存储类型可以用于频繁使用的变量。实际上现在一般的编译器都忽略auto和register申明,现在的编译器自己能够区分最好将那些变量放置在寄存器中,那些放置在堆栈中;甚至于将一些变量有时存放在堆栈,有时存放在寄存器中。

    使用register修饰符有几点限制

    (1)register变量必须是能被CPU所接受的类型。这通常意味着register变量必须是一个单个的值,并且长度应该小于或者等于整型的长度。不过,有些机器的寄存器也能存放浮点数。最好不要这样去用

    (2)因为register变量可能不存放在内存中,所以不能用“&”来获取register变量的地址。

    (3)只有局部自动变量和形式参数可以作为寄存器变量,其它(如全局变量)不行。在调用一个函数时占用一些寄存器以存放寄存器变量的值,函数调用结束后释放寄存器。此后,在调用另外一个函数时又可以利用这些寄存器来存放该函数的寄存器变量。所以说不要用register修饰全局变量等,因为他长时间的占用寄存器不允许再被使用了。

    (4)局部静态变量不能定义为寄存器变量。不能写成:register static int a, b, c,同样的道理,因为static变量函数结束不会被销毁,下面进入还会使用之前的数据,生命周期直到程序退出才结束,数据存放在静态区。

    (5)由于寄存器的数量有限(不同的cpu寄存器数目不一),不能定义任意多个寄存器变量,而且某些寄存器只能接受特定类型的数据(如指针和浮点数),因此真正起作用的register修饰符的数目和类型都依赖于运行程序的机器,而任何多余的register修饰符都将被编译程序所忽略。

    auto register 是用来修饰变量的,static extern 变量函数都可以

    Linux IO 读写函数

    Linux IO 读取分两种类型,一种是本地文件的读取,一种是网络通信的读取

    本地文件的读取函数

    对于本地文件的读取,Linux有两种方式,一种是有缓存的,另一种是没有缓存的。

    带缓存的文件读取

    该类型会在内存开辟一个“缓冲区”,为程序中的每一个文件使用;当执行读文件的操作时,从磁盘文件将数据先读入内存“缓冲区”,装满后再从内存“缓冲区”依此读出需要的数据。执行写文件的操作时,先将数据写入内存“缓冲区”,待内存“缓冲区”装满后再写入文件。由此可以看出,内存“缓冲区”的大小,影响着实际操作外存的次数,内存“缓冲区”越大,则操作外存的次数就少,执行速度就快、效率高。一般来说,文件“缓冲区”的大小随机器而定。

    主要的函数有:fopen, fclose, fread, fwrite, fgetc, fgets, fputc, fputs, freopen, fseek, ftell, rewind等。

    不带缓存的文件读取

    依赖于操作系统,通过操作系统的功能对文件进行读写,是系统级的输入输出,它不设文件结构体指针,只能读写二进制文件,但效率高、速度快。主要的函数有:open, close, read, write, getc, getchar, putc, putchar, feof, ferror

    两者区别

    来源

    1. open是UNIX系统调用函数(包括LINUX等),返回的是文件描述符(File Descriptor),它是文件在文件描述符表里的索引。
    2. fopen是ANSIC标准中的C语言库函数,在不同的系统中应该调用不同的内核api。返回的是一个指向文件结构的指针。

    移植性

    这一点从上面的来源就可以推断出来,fopen是C标准函数,因此拥有良好的移植性;而open是UNIX系统调用,移植性有限。如windows下相似的功能使用API函数CreateFile

    适用范围

    1. open返回文件描述符,而文件描述符是UNIX系统下的一个重要概念,UNIX下的一切设备都是以文件的形式操作。如网络套接字、硬件设备等。当然包括操作普通正规文件(Regular File)。
    2. fopen是用来操纵普通正规文件(Regular File)的。

    文件IO层次

    如果从文件IO的角度来看,前者属于低级IO函数,后者属于高级IO函数。低级和高级的简单区分标准是:谁离系统内核更近。低级文件IO运行在内核态,高级文件IO运行在用户态。

    使用fopen函数,由于在用户态下就有了缓冲,因此进行文件读写操作的时候就减少了用户态和内核态的切换(切换到内核态调用还是需要调用系统调用API:read,write);而使用open函数,在文件读写时则每次都需要进行内核态和用户态的切换;表现为,如果顺序访问文件,fopen系列的函数要比直接调用open系列的函数快;如果随机访问文件则相反。

    网络IO的读取

    网络IO的读取主要针对的是socket,主要有:

    1
    2
    3
    4
    5
    ssize_t write(int fd, const void*buf,size_t nbytes);
    ssize_t read(int fd,void *buf,size_t nbyte)

    int recv(int sockfd,void *buf,int len,int flags)
    int send(int sockfd,void *buf,int len,int flags)

    recv和send与read和write的不同就是函数参数多了一个标志,该标志如果为0的话,那么其作用就相当于read和write函数了。
    flag的取值可以为0或是下面的组合:

    MSG_DONTROUTE:是send函数使用的标志.这个标志告诉IP.目的主机在本地网络上面,没有必要查找表.这个标志一般用网络诊断和路由程序里面.
    MSG_OOB:表示可以接收和发送带外的数据.关于带外数据我们以后会解释的.

    MSG_PEEK:是recv函数的使用标志,表示只是从系统缓冲区中读取内容,而不清除系统缓冲区的内容.这样下次读的时候,仍然是一样的内容.一般在有多个进程读写数据时可以使用这个标志.

    MSG_WAITALL是recv函数的使用标志,表示等到所有的信息到达时才返回.使用这个标志的时候recv回一直阻塞,直到指定的条件满足,或者是发生了错误. 1)当读到了指定的字节时,函数正常返回.返回值等于len 2)当读到了文件的结尾时,函数正常返回.返回值小于len 3)当操作发生错误时,返回-1,且设置错误为相应的错误号(errno)

    以上四个函数只适合与面向连接的套接字。对于UDP这样的非连接,需要使用下面的函数

    1
    2
    3
    4
    #include "sys/socket.h"

    ssize_t recvmsg(int sockfd, struct msghdr * msg, int flags);
    ssize_t sendmsg(int sockfd, struct msghdr * msg, int flags);

    成功时候返回读写字节数,出错时候返回-1.

    这2个函数只用于套接口,不能用于普通的I/O读写,参数sockfd则是指明要读写的套接口。
    flags用于传入控制信息,一般包括以下几个
    MSG_DONTROUTE send可用
    MSG_DONWAIT send与recv都可用
    MSG_PEEK recv可用
    MSG_WAITALL recv可用
    MSG_OOB send可用
    MSG_EOR send recv可用

    返回信息都记录在struct msghdr * msg中。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    struct msghdr {
    //协议地址和套接口信息,在非连接的UDP中,发送者要指定对方地址端口,接受方用于的到数据来源,如果不需要的话
    //可以设置为NULL(在TCP或者连接的UDP中,一般设置为NULL)。
    void * msg_name;
    socklen_t msg_namelen;//上面的长度
    struct lovec * msg_lov;
    ssize_t msg_lovlen;//和readv和writev一样
    void * msg_control;
    socklen_t msg_controllen;
    int msg_flags; //用于返回之前flags的控制信息
    }

    下面是该函数使用的例子:

    下面的源码来自这里

    服务器

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    #define MAXSIZE 100

    int main(int argc, char ** argu) {
    .......
    struct msghdr msg;//初始化struct msghdr
    msg.msg_name = NULL; //在tcp中,可以设置为NULL
    struct iovec io;//初始化返回数据
    io.iov_base = buf; //只用了一个缓冲区
    io.iov_len = MAXSIZE; //定义返回数据长度
    msg.msg_iov = &io;
    msg.msg_iovlen = 1;//只用了一个缓冲区,所以长度为1

    ...................
    ssize_t recv_size = recvmsg(connfd, &msg, 0);
    char * temp = msg.msg_iov[0].iov_base;//获取得到的数据
    temp[recv_size] = '\0';//为数据末尾添加结束符
    printf("get message:%s", temp);

    }

    客户端

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    #define MAXSIZE 100

    int main(int argc, char ** argv) {
    .................
    struct msghdr msg;//初始化发送信息
    msg.msg_name = NULL;
    struct iovec io;
    io.iov_base = send_buff;
    io.iov_len = sizeof(send_buff);
    msg.msg_iov = &io;
    msg.msg_iovlen = 1;

    if(argc != 2) {
    printf("please input port");
    exit(1);
    }

    ssize_t size = sendmsg(sockfd, &msg, 0);
    close(sockfd);
    exit(0);
    }

    这里控制信息都设置成0,主要是初始化返回信息struct msghdr结构。

    未连接的UDP套接口
    服务器

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    #include "/programe/net/head.h"
    #include "stdio.h"
    #include "stdlib.h"
    #include "string.h"
    #include "unistd.h"
    #include "sys/wait.h"
    #include "sys/select.h"
    #include "sys/poll.h"

    #define MAXSIZE 100

    int main(int argc, char ** argv) {
    int sockfd;
    struct sockaddr_in serv_socket;
    struct sockaddr_in *client_socket = (struct sockaddr_in *) malloc (sizeof(struct sockaddr_in));
    char buf[MAXSIZE + 1];

    sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    bzero(&serv_socket, sizeof(serv_socket));
    serv_socket.sin_family = AF_INET;
    serv_socket.sin_addr.s_addr = htonl(INADDR_ANY);
    serv_socket.sin_port = htons(atoi(argv[1]));
    bind(sockfd, (struct sockaddr *)&serv_socket, sizeof(serv_socket));

    struct msghdr msg;
    msg.msg_name = client_socket;
    //如果想得到对方的地址和端口,一定要把初始化完毕的内存头指针放入msg之中
    msg.msg_namelen = sizeof(struct sockaddr_in);//长度也要指定
    struct iovec io;
    io.iov_base = buf;
    io.iov_len = MAXSIZE;
    msg.msg_iov = &io;
    msg.msg_iovlen = 1;

    ssize_t len = recvmsg(sockfd, &msg, 0);
    client_socket = (struct sockaddr_in *)msg.msg_name;
    char ip[16];
    inet_ntop(AF_INET, &(client_socket->sin_addr), ip, sizeof(ip));
    int port = ntohs(client_socket->sin_port);
    char * temp = msg.msg_iov[0].iov_base;
    temp[len] = '\0';
    printf("get message from %s[%d]: %s\n", ip, port, temp);
    close(sockfd);
    }

    客户端

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    #include "/programe/net/head.h"
    #include "stdio.h"
    #include "stdlib.h"
    #include "string.h"
    #include "sys/select.h"

    #define MAXSIZE 100

    int main(int argc, char ** argv) {
    int sockfd;
    struct sockaddr_in serv_socket;
    int maxfdpl;
    char send[] = "hello yuna";
    if(argc != 2) {
    printf("please input port");
    exit(1);
    }

    sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    bzero(&serv_socket, sizeof(serv_socket));
    serv_socket.sin_family = AF_INET;
    serv_socket.sin_port = htons(atoi(argv[1]));
    inet_pton(AF_INET, "192.168.1.235", &serv_socket.sin_addr);

    struct msghdr msg;
    msg.msg_name = &serv_socket;
    msg.msg_namelen = sizeof(struct sockaddr_in);
    struct iovec io;
    io.iov_base = send;
    io.iov_len = sizeof(send);
    msg.msg_iov = &io;
    msg.msg_iovlen = 1;

    ssize_t send_size = sendmsg(sockfd, &msg, 0);
    close(sockfd);
    exit(0);
    }

    sendto和recvfrom

    1
    2
    3
    4
    5
    6
    #include <sys/types.h>
    #include <sys/socket.h>

    int sendto (int s, const void *buf, int len, unsigned int flags, const struct sockaddr *to, int tolen);

    int recvfrom(int s, void *buf, int len, unsigned int flags, struct sockaddr *from, int *fromlen);

    对于sendto()函数,成功则返回实际传送出去的字符数,失败返回-1,错误原因存于errno 中。

    对于recvfrom()函数,成功则返回接收到的字符数,失败则返回-1,错误原因存于errno中。

    如果需要在TCP的socket中使用该函数,那么可以直接的对函数最后的两个参数设为NULL。

    UDP Server和Client源码实例:

    一下源码来自这里
    服务端:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    #include <sys/types.h>
    #include <sys/socket.h>
    #include <netinet/in.h>
    #include <arpa/inet.h>
    #include <unistd.h>
    #include <stdlib.h>
    #include <string.h>
    #include <stdio.h>

    #define UDP_TEST_PORT 50001

    int main(int argC, char* arg[])
    {

    struct sockaddr_in addr;
    int sockfd, len = 0;
    int addr_len = sizeof(struct sockaddr_in);
    char buffer[256];

    /* 建立socket,注意必须是SOCK_DGRAM */
    if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {
    perror ("socket");
    exit(1);
    }

    /* 填写sockaddr_in 结构 */
    bzero(&addr, sizeof(addr));
    addr.sin_family = AF_INET;
    addr.sin_port = htons(UDP_TEST_PORT);
    addr.sin_addr.s_addr = htonl(INADDR_ANY) ;// 接收任意IP发来的数据

    /* 绑定socket */
    if (bind(sockfd, (struct sockaddr *)&addr, sizeof(addr))<0) {
    perror("connect");
    exit(1);
    }

    while(1) {
    bzero(buffer, sizeof(buffer));
    len = recvfrom(sockfd, buffer, sizeof(buffer), 0,
    (struct sockaddr *)&addr ,&addr_len);
    /* 显示client端的网络地址和收到的字符串消息 */
    printf("Received a string from client %s, string is: %s\n",
    inet_ntoa(addr.sin_addr), buffer);
    /* 将收到的字符串消息返回给client端 */
    sendto(sockfd,buffer, len, 0, (struct sockaddr *)&addr, addr_len);
    }

    return 0;
    }

    // ----------------------------------------------------------------------------
    // End of udp_server.c

    UDP 客户端

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    #include <sys/types.h>
    #include <sys/socket.h>
    #include <netinet/in.h>
    #include <arpa/inet.h>
    #include <unistd.h>
    #include <stdlib.h>
    #include <string.h>
    #include <stdio.h>

    #define UDP_TEST_PORT 50001
    #define UDP_SERVER_IP "127.0.0.1"

    int main(int argC, char* arg[])
    {

    struct sockaddr_in addr;
    int sockfd, len = 0;
    int addr_len = sizeof(struct sockaddr_in);
    char buffer[256];

    /* 建立socket,注意必须是SOCK_DGRAM */
    if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {
    perror("socket");
    exit(1);
    }

    /* 填写sockaddr_in*/
    bzero(&addr, sizeof(addr));
    addr.sin_family = AF_INET;
    addr.sin_port = htons(UDP_TEST_PORT);
    addr.sin_addr.s_addr = inet_addr(UDP_SERVER_IP);

    while(1) {
    bzero(buffer, sizeof(buffer));

    printf("Please enter a string to send to server: \n");

    /* 从标准输入设备取得字符串*/
    len = read(STDIN_FILENO, buffer, sizeof(buffer));

    /* 将字符串传送给server端*/
    sendto(sockfd, buffer, len, 0, (struct sockaddr *)&addr, addr_len);

    /* 接收server端返回的字符串*/
    len = recvfrom(sockfd, buffer, sizeof(buffer), 0,
    (struct sockaddr *)&addr, &addr_len);
    printf("Receive from server: %s\n", buffer);
    }

    return 0;
    }

    // ----------------------------------------------------------------------------
    // End of udp_client.c

    Boost智能指针

    Boost 只能指针常用的有四种,分别为:shared_ptr、weak_ptr、auto_ptr以及scoped_ptr指针。

    shared_ptr指针

    shared_ptr指针就是所谓的只能计数指针,用来管理非栈上的内存指针。它可以从一个裸指针、另一个shared_ptr、一个auto_ptr、或者一个weak_ptr构造。还可以传递第二个参数给shared_ptr的构造函数,它被称为删除器(deleter)。删除器用于处理共享资源的释放,这对于管理那些不是用new分配也不是用delete释放的资源时非常有用。shared_ptr被创建后,就可以像普通指针一样使用了,除了一点,它不能被显式地删除。

    shared_ptr指针的使用如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    {
    shared_ptr<int> pInt1;
    assert(pInt1.use_count() == 0); // 还没有引用指针
    {
    shared_ptr<int> pInt2(new int(5));
    assert(pInt2.use_count() == 1); // new int(5)这个指针被引用1次

    pInt1 = pInt2;
    assert(pInt2.use_count() == 2); // new int(5)这个指针被引用2次
    assert(pInt1.use_count() == 2);
    } //pInt2离开作用域, 所以new int(5)被引用次数-1

    assert(pInt1.use_count() == 1);
    } // pInt1离开作用域,引用次数-1,现在new int(5)被引用0次,所以销毁它

    如果资源的创建销毁不是以new和delete的方式进行的,该怎么办呢?通过前面的接口可以看到,shared_ptr的构造函数中可以指定删除器。示例代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    class FileCloser
    {
    public:
    void operator()(FILE \*pf)
    {

    if (pf != NULL)
    {
    fclose(pf);
    pf = NULL;
    }
    }
    };

    shared_ptr<FILE> fp(fopen(pszConfigFile, "r"), FileCloser());

    在使用shared_ptr时,需要避免同一个对象指针被两次当成shard_ptr构造函数里的参数的情况。考虑如下代码:

    1
    2
    3
    4
    5
    6
    7
    {
    int *pInt = new int(5);
    shared_ptr<int> temp1(pInt);
    assert(temp1.use_count() == 1);
    shared_ptr<int> temp2(pInt);
    assert(temp2.use_count() == 1);
    } // temp1和temp2都离开作用域,它们都销毁pInt,会导致两次释放同一块内存

    正确的做法是将原始指针赋给智能指针后,以后的操作都要针对智能指针了。参考代码如下:

    1
    2
    3
    4
    5
    6
    {
    shared_ptr<int> temp1(new int(5));
    assert(temp1.use_count() == 1);
    shared_ptr<int> temp2(temp1);
    assert(temp2.use_count() == 2);
    } // temp1和temp2都离开作用域,引用次数变为0,指针被销毁。

    另外,使用shared_ptr来包装this时,也会产生与上面类似的问题。考虑如下代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    class A
    {
    public:
    shared_ptr<A> Get()
    {
    return shared_ptr<A>(this);
    }
    }

    shared_ptr<A> pA(new A());
    shared_ptr<A> pB = pA->Get();

    当pA和pB离开作用域时,会将堆上的对象释放两次。如何解决上述问题呢?C++ 11提供了如下机制:将类从enable_shared_from_this类派生,获取shared_ptr时使用shared_from_this接口。参考代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    class A :public enable_shared_from_this<A>
    {
    public:
    shared_ptr<A> Get()
    {
    return shared_from_this();
    }
    }

    在多线程中使用shared_ptr时,如果存在拷贝或赋值操作,可能会由于同时访问引用计数而导致计数无效。解决方法是向每个线程中传递公共的week_ptr,线程中需要使用shared_ptr时,将week_ptr转换成shared_ptr即可。
    以上例子来自这里

    weak_ptr指针

    weak_ptr是为配合shared_ptr而引入的一种智能指针,它更像是shared_ptr的一个助手,而不是智能指针,因为它不具有普通指针的行为,没有重载operator*和operator->,它的最大作用在于协助shared_ptr,像旁观者那样观测资源的使用情况。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    template<class T> class weak_ptr{  
    public:
    weak_ptr();
    template<class Y> weak_ptr(shared_ptr<Y> const & r);
    weak_ptr(weak_ptr const & r);

    ~weak_ptr();
    weak_ptr & operator=(weak_ptr const &r);

    long use_count() const;
    bool expired() const;
    shared_ptr<T> lock() const;

    void reset();
    void swap(weak_ptr<T> &b);
    };

    weak_ptr是一个“弱”指针,但它能够完成一些特殊的工作,足以证明它的存在价值。

    weak_ptr被设计为与shared_ptr共同工作,可以从一个shared_ptr或者另一个weak_ptr对象构造,获得资源的观测权。但weak_ptr没有共享资源,它的构造不会引起指针引用计数的增加。同样,在weak_ptr析构时也不会导致引用计数的减少,它只是一个静静地观察者。

    使用weak_ptr的成员函数use_count()可以观测资源的引用计数,另一个成员函数expired()的功能等价于use_count() == 0,但更快,表示观测的资源(也就是shared_ptr管理的资源)已经不复存在了。

    weak_ptr 没有重载operator*和->,这是特意的,因为它不共享指针,不能操作资源,这是它弱的原因。但它可以使用一个非常重要的成员函数lock()从被观测的shared_ptr获得一个可用的shared_ptr对象,从而操作资源。当expired() == true的时候,lock()函数将返回一个存储空指针的shared_ptr。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    int main(){  
    shared_ptr<int> sp(new int(10));
    assert(sp.use_count() == 1);
    //create a weak_ptr from shared_ptr
    weak_ptr<int> wp(sp);
    //not increase the use count
    assert(sp.use_count() == 1);
    //judge wp is invalid
    //expired() is equivalent with use_count() == 0
    if(!wp.expired()){
    shared_ptr<int> sp2 = wp.lock();//get a shared_ptr
    \*sp2 = 100;
    assert(wp.use_count() == 2);
    cout << \*sp2 << endl;
    }//out of scope,sp2 destruct automatically,use_count()--;
    assert(wp.use_count() == 1);
    sp.reset();//shared_ptr is invalid
    assert(wp.expired());
    assert(!wp.lock());
    }

    获得this的shared_ptr

    weak_ptr的一个重要用途是获得this指针的shared_ptr,使对象自己能够生产shared_ptr管理自己:对象使用weak_ptr观测this指,这并不影响引用计数,在需要的时候就调用lock()函数,返回一个符合要求的shared_ptr使外界使用。

    这个解决方案被实现为一个惯用法,在头文件定义了一个助手类enable_shared_from_this,其声明如下:

    1
    2
    3
    4
    5
    6
    template<class T>  
    class enable_shared_from_this
    {
    public:
    shared_ptr<T> shared_from_this();
    }

    使用的时候只需要让想被shared_ptr管理的类从它继承即可,成员函数shared_from_this()会返回this的shared_ptr

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    #include <iostream>  
    #include <boost/smart_ptr.hpp>
    #include <boost/enable_shared_from_this.hpp>
    #include <boost/make_shared.hpp>
    using namespace boost;
    using namespace std;
    class self_shared:
    public enable_shared_from_this<self_shared>{
    public:
    self_shared(int n):x(n){}
    int x;
    void print(){
    cout << "self_shared:" << x << endl;
    }
    };
    int main(){
    shared_ptr<self_shared> sp =
    make_shared<self_shared>(315);
    sp->print();
    shared_ptr<self_shared> p = sp->shared_from_this();
    p->x = 100;
    p->print();

    }

    运行结果:

    1
    2
    self_shared:315
    self_shared:100

    需要注意的是千万不能从一个普通对象(非shared_ptr)使用shared_from_this ()获取shared_ptr,如

    1
    2
    3
    self_shared ss;

    shaerd_ptr<self_shared> p = ss.shared_from_this();//error

    这样虽然语法上能通过,编译也无问题,但在运行时会导致shared_ptr析构时企图删除一个栈上分配的对象,发生未定义行为。

    以上内容来自这里

    auto_ptr指针

    auto_ptr通过在栈上构建一个对象a,对象a中wrap了动态分配内存的指针p,所有对指针p的操作都转为对对象a的操作。而在a的析构函数中会自动释放p的空间,而该析构函数是编译器自动调用的,无需程序员操心。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    用法一:  
    std::auto_ptr<MyClass>m_example(new MyClass());

    用法二:
    std::auto_ptr<MyClass>m_example;
    m_example.reset(new MyClass());

    用法三(指针的赋值操作):
    std::auto_ptr<MyClass>m_example1(new MyClass());
    std::auto_ptr<MyClass>m_example2(new MyClass());
    m_example2=m_example1;
    ```

    对于上面的代码:m_example2=m_example1; 则C++会把m_example所指向的内存回收,使m_example1 的值为NULL,所以在C++中,应绝对避免把auto_ptr放到容器中。即应避免下列代码:
    vector>m_example;
    当用算法对容器操作的时候,你很难避免STL内部对容器中的元素实现赋值传递,这样便会使容器中多个元素被置位NULL,而这不是我们想看到的。

    示例:

    1
    2
    3
    4
    5
    6
    7
    // 示例1(a):原始代码    
    void f()
    {

    T* pt( new T );
    /*...更多的代码...*/
    delete pt;
    }

    以上代码需要手动的delete掉堆上的内存,如果使用auto_ptr指针的话,就不需要:

    1
    2
    3
    4
    5
    6
    // 示例1(b):安全代码,使用了auto_ptr  
    void f()
    {

    auto_ptr<T> pt( new T );
    /*...更多的代码...*/
    } // 酷:当pt出了作用域时析构函数被调用,从而对象被自动删除

    使用一个auto_ptr就像使用一个内建的指针一样容易,而且如果想要“撤销”资源,重新采用手动的所有权,我们只要调用release()。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    // 示例2:使用一个auto_ptr  
    void g()
    {

    // 现在,我们有了一个分配好的对象
    T* pt1 = new T;
    // 将所有权传给了一个auto_ptr对象
    auto_ptr<T> pt2(pt1);
    // 使用auto_ptr就像我们以前使用简单指针一样,
    *pt2 = 12; // 就像*pt1 = 12
    pt2->SomeFunc(); // 就像pt1->SomeFunc();
    // 用get()来获得指针的值
    assert( pt1 == pt2.get() );
    // 用release()来撤销所有权
    T* pt3 = pt2.release();
    // 自己删除这个对象,因为现在没有任何auto_ptr拥有这个对象
    delete pt3;
    } // pt2不再拥有任何指针,所以不要试图删除它...OK,不要重复删除

    ```

    我们可以使用auto_ptr的reset()函数来重置auto_ptr使之拥有另一个对象。如果这个auto_ptr已经拥有了一个对象,那么,它会先删除已经拥有的对象,因此调用reset()就如同销毁这个auto_ptr,然后新建一个并拥有一个新对象:

    1
    2
    3
    4
    5
    6
    // 示例 3:使用reset()  
    void h()
    {

    auto_ptr<T> pt( new T(1) );
    pt.reset( new T(2) ); // 删除由"new T(1)"分配出来的第一个T
    } // 最后pt出了作用域,第二个T也被删除了

    以上原文来自这里

    scoped_ptr指针

    scoped_ptr和std::auto_ptr非常类似,是一个简单的智能指针,它能够保证在离开作用域后对象被自动释放。下列代码演示了该指针的基本应用:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    #include <string>
    #include <iostream>
    #include <boost/scoped_ptr.hpp>

    class implementation
    {
    public:
    ~implementation() { std::cout <<"destroying implementation\n"; }
    void do_something() { std::cout << "did something\n"; }
    };

    void test()
    {

    boost::scoped_ptr<implementation> impl(new implementation());
    impl->do_something();
    }

    void main()
    {

    std::cout<<"Test Begin ... \n";
    test();
    std::cout<<"Test End.\n";
    }

    该代码的输出结果是:

    1
    2
    3
    4
    Test Begin ...
    did something
    destroying implementation
    Test End.

    可以看到:当implementation类离其开impl作用域的时候,会被自动删除,这样就会避免由于忘记手动调用delete而造成内存泄漏了。

    scoped_ptr的实现和std::auto_ptr非常类似,都是利用了一个栈上的对象去管理一个堆上的对象,从而使得堆上的对象随着栈上的对象销毁时自动删除。不同的是,boost::scoped_ptr有着更严格的使用限制——不能拷贝。这就意味着:boost::scoped_ptr指针是不能转换其所有权的。

    1. 不能转换所有权
      boost::scoped_ptr所管理的对象生命周期仅仅局限于一个区间(该指针所在的”{}”之间),无法传到区间之外,这就意味着boost::scoped_ptr对象是不能作为函数的返回值的(std::auto_ptr可以)。

    2. 不能共享所有权
      这点和std::auto_ptr类似。这个特点一方面使得该指针简单易用。另一方面也造成了功能的薄弱——不能用于stl的容器中。

    3. 不能用于管理数组对象
      由于boost::scoped_ptr是通过delete来删除所管理对象的,而数组对象必须通过deletep[]来删除,因此boost::scoped_ptr是不能管理数组对象的,如果要管理数组对象需要使用boost::scoped_array类。

    scoped_ptr的定义:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    namespace boost {

    template<typename T> class scoped_ptr : noncopyable {
    public:
    explicit scoped_ptr(T* p = 0);
    ~scoped_ptr();

    void reset(T* p = 0);

    T& operator*() const;
    T* operator->() const;
    T* get() const;

    void swap(scoped_ptr& b);
    };

    template<typename T>
    void swap(scoped_ptr<T> & a, scoped_ptr<T> & b);
    }
    ```

    示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    #include <string>
    #include <iostream>

    #include <boost/scoped_ptr.hpp>
    #include <boost/scoped_array.hpp>

    #include <boost/config.hpp>
    #include <boost/detail/lightweight_test.hpp>

    void test()
    {

    // test scoped_ptr with a built-in type
    long * lp = new long;
    boost::scoped_ptr<long> sp ( lp );
    BOOST_TEST( sp.get() == lp );
    BOOST_TEST( lp == sp.get() );
    BOOST_TEST( &\*sp == lp );

    \*sp = 1234568901L;
    BOOST_TEST( \*sp == 1234568901L );
    BOOST_TEST( \*lp == 1234568901L );

    long * lp2 = new long;
    boost::scoped_ptr<long> sp2 ( lp2 );

    sp.swap(sp2);
    BOOST_TEST( sp.get() == lp2 );
    BOOST_TEST( sp2.get() == lp );

    sp.reset(NULL);
    BOOST_TEST( sp.get() == NULL );

    }

    void main()
    {

    test();
    }

    boost::scoped_ptr和std::auto_ptr的选取:

    boost::scoped_ptr和std::auto_ptr的功能和操作都非常类似,如何在他们之间选取取决于是否需要转移所管理的对象的所有权(如是否需要作为函数的返回值)。如果没有这个需要的话,大可以使用boost::scoped_ptr,让编译器来进行更严格的检查,来发现一些不正确的赋值操作。

    以上原文来自这里

    Linux任务调度机制

    在Linux中,每一个CPU都会有一个队列来存储处于TASK_RUNNING状态的任务,任务调度就是从这些队列中取出优先级最高的任务作为下一个放入CPU执行的任务。

    任务的调度需要进过两个过程:上下文切换和选择算法

    上下文切换

    从一个进程的上下文切换到另一个进程的上下文,因为其发生频率很高,所以通常都是调度器效率高低的关键。schedule()函数中调用了switch_to宏,这个宏实现了进程之间的真正切换,其代码存放于include/i386/system.h。switch_to宏是用嵌入式汇编写成的,较难理解。由switch_to()实现,而它的代码段在schedule()过程中调用,以一个宏实现。switch_to()函数正常返回,栈上的返回地址是新进程的task_struct::thread::eip,即新进程上一次被挂起时设置的继续运行的位置(上一次执行switch_to()时的标号”1:”位置)。至此转入新进程的上下文中运行。这其中涉及到wakeup,sleepon等函数来对进程进行睡眠与唤醒操作。

    选择算法

    Linux schedule()函数将遍历就绪队列中的所有进程,调用goodness()函数计算每一个进程的权值weight,从中选择权值最大的进程投入运行。Linux的调度器主要实现在schedule()函数中。

    调度步骤:

    Schedule函数工作流程如下:

    (1)清理当前运行中的进程
    (2)选择下一个要运行的进程(pick_next_task)
    (3)设置新进程的运行环境
    (4) 进程上下文切换

    Linux 调度器将进程分为三类

    进程调度是操作系统的核心功能。调度器只是调度过程中的一部分,进程调度是非常复杂的过程,需要多个系统协同工作完成。本文所关注的仅为调度器,它的主要工作是在所有RUNNING 进程中选择最合适的一个。作为一个通用操作系统,Linux 调度器将进程分为三类:

    1. 交互式进程
      此类进程有大量的人机交互,因此进程不断地处于睡眠状态,等待用户输入。典型的应用比如编辑器 vi。此类进程对系统响应时间要求比较高,否则用户会感觉系统反应迟缓。

    2. 批处理进程
      此类进程不需要人机交互,在后台运行,需要占用大量的系统资源。但是能够忍受响应延迟。比如编译器。

    3. 实时进程
      实时对调度延迟的要求最高,这些进程往往执行非常重要的操作,要求立即响应并执行。比如视频播放软件或飞机飞行控制系统,很明显这类程序不能容忍长时间的调度延迟,轻则影响电影放映效果,重则机毁人亡。

    调度时机:调度什么时候发生?即:schedule()函数什么时候被调用?

    调度的发生主要有两种方式:

    1:主动式调度(自愿调度)

    在内核中主动直接调用进程调度函数schedule(),当进程需要等待资源而暂时停止运行时,会把状态置于挂起(睡眠),并主动请求调度,让出cpu。

    2:被动式调度(抢占式调度、强制调度)

    用户抢占(2.4 2.6)
    内核抢占(2.6)

    (1)用户抢占发生在:从系统调用返回用户空间;从中断处理程序返回用户空间。

    内核即将返回用户空间的时候,如果need_resched标志被设置,会导致schedule()被调用,此时就会发生用户抢占。主动式调度是用户程序自己调度schedule,也许有人会觉得自己的代码中能引用schedule吗?也许不行吧,但大家知道wait4我们是可以调用的,前面我们没有给出wait4的代码,但我们知道在执行了wait4效果是父进程被挂起,所谓的挂起就是不运行了,放弃了CPU,这里发生了进程调度是显而易见的,其实在代码中有如下几行:

    1
    current­>state = TASK_INTERRUPIBLE;schedule();

    还有exit也有

    1
    current­>state = TASK_ZOMBIE; schedule();

    这2种发生了进程调度,从代码上也可以看出(状态被改成了睡眠和僵死,然后去调度可运行进程,当前进程自然不会再占有CPU运行了),从效果中也能看出。这说明用户程序自己可以执行进程调度。

    (2)内核抢占

    在不支持内核抢占的系统中,进程/线程一旦运行于内核空间,就可以一直执行,直到它主动放弃或时间片耗尽为止。这样一些非常紧急的进程或线程将长时间得不到运行。在支持内核抢占的系统中,更高优先级的进程/线程可以抢占正在内核空间运行的低优先级的进程/线程。关于抢占式调度(强制调度),需要知道的是,CPU在执行了当前指令之后,在执行下一条指令之前,CPU要判断在当前指令执行之后是否发生了中断或异常,如果发生了,CPU将比较到来的中断优先级和当前进程的优先级(有硬件参与实现,如中断控制器8259A芯片;通过比较寄存器的值来判断优先级;中断服务程序的入口地址形成有硬件参与实现,等等,具体实现请见相关资料和书籍),如果新来任务的优先级更高,则执行中断服务程序,在返回中断时,将执行进程调度函数schedule。

    在支持内核抢占的系统中,某些特例下是不允许内核被抢占的:
    (a)内核正在运行中断处理程序,进程调度函数schedule()会对此作出判断,如果是在中断中调用,会打印出错误信息。

    (b) 内核正在进行中断上下文的bottom half(中断的底半部)处理,硬件中断返回前会执行软中断,此时仍然处于中断上下文。

    (c) 进程正持有spinlock自旋锁,writelock/readlock读写锁等,当持有这些锁时,不应该被抢占,否则由于抢占将导致其他cpu长时间不能获得锁而死锁。

    (d) 内核正在执行调度程序scheduler

    为了保证linux内核在以上情况下不会被抢占,抢占式内核使用了一个变量preempt_count,称为内核抢占计数。这一变量被设置在进程的thread_info结构体中,每当内核要进入以上几种状态时,变量preempt_count就加1,指示内核不允许抢占,反之减1。

    Linux任务调度策略

    Linux支持SCHED_FIFO、SCHED_RR和SCHED_OTHER的调度策略。

    linux用函数goodness()统一计算进程(包括普通进程和实时进程)的优先级权值,该权值衡量一个处于可运行状态的进程值得运行的程度,权值越大,进程优先级越高。 每个进程的task_struct结构中,与goodness()计算权值相关的域有以下四项:policy、nice(2.2版内核该项为priority)、counter、rt_priority。其中,policy是进程的调度策略,其可用来区分实时进程和普通进程,实时进程优先于普通进程运行。nice从最初的UNIX沿用而来,表示进程的静态负向优先级,其取值范围为19~-20,以-20优先级最高。counter表示进程剩余的时间片计数值,由于counter在计算goodness()时起重要作用,因此,counter也可以看作是进程的动态优先级。rt_priority是实时进程特有的,表示实时优先级。

    首先,linux根据调度策略policy从整体上区分实时进程和普通进程。对于policy为SCHED_OTHER的普通进程,linux采用动态优先级调,其优先级权值取决于(20-nice)和进程当前的剩余时间片计数counter之和。进程创建时,子进程继承父进程的nice值,而父进程的counter值则被分为二半,子进程和父进程各得一半。时间片计数器每次清零后由(20-nice)经过换算重新赋值。字面上看,nice是“优先级”、counter是“计数器”的意思,然而实际上,它们表达的是同个意思:nice决定了分配给该进程的时间片计数,nice优先级越高的进程分到的时间片越长,用户通过系统调用nice()或setpriority()改变进程静态优先级nice值的同时,也改变了该进程的时间片长度;counter表示该进程剩余的时间片计数值,而nice和counter综合起来又决定进程可运行的优先级权值。在进程运行过程中,counter不断减少,而nice保持相对不变;当一个普通进程的时间片用完以后,并不马上根据nice对counter进行重新赋值,只有所有处于可运行状态的普通进程的时间片都用完了以后(counter等于0),才根据nice对counter重新赋值,这个普通进程才有了再次被调度的机会。这说明,普通进程运行过程中,counter的减小给了其它进程得以运行的机会,直至counter减为0时才完全放弃对CPU的使用,这就相当于优先级在动态变化,所以称之为动态优先调度。

    对于实时进程,linux采用了两种调度策略,即SCHED_FIFO(先来先服务调度)和SCHED_RR(时间片轮转调度)。因为实时进程具有一定程度的紧迫性,所以衡量一个实时进程是否应该运行,采用了一个比较固定的标准,即参考rt_priority的值。用函数goodness()计算进程的优先级权值时,对实时进程是在1000的基础上加上rt_priority的值,而非实时进程的动态优先级综合起来的调度权值始终在以下,所以goodness()的优先级权值计算方法确保实