Principles of Computer Systems
Spring 2019
Stanford University
Computer Science Department
Lecturer: Chris Gregg
waitpid
), for something to become available (e.g. read
from a client socket that's not seen any data recently), or for some external event (e.g. network connection request from client via accept
.)
read
are considered fast if they're reading from a local file, because there aren't really any external resources to prevent read
from doing it's work. It's true that some hardware needs to be accessed, but because that hardware is grafted into the machine, we can say with some confidence that the data being read from the local file will be available within a certain amount of time.write
calls are slow if data is being published to a socket and previously published data has congested internal buffers and not been pushed off the machine yet.waitpid
out of the normal flow of execution, and we've also relied on WNOHANG
to ensure that waitpid
never actually blocks.
read
and write
—calls to embedded within iosockstream
operations—off the main thread.
read
and write
any faster, but it does parallelize the stall times free up the main thread so that it can work on other things.aggregate
and proxy
.accept
and read
are the I/O system calls that everyone always identifies as slow.accept
, it'll always return as quickly as possible.
accept
is called, it'll return immediately with a socket connection to that client.accept
will return -1 instead of blocking if there are no pending connection requests. The -1 normally denotes that some error occurred, but if errno
is set to EWOULDBLOCK
, the -1 isn't really identifying an error, but instead saying that accept
would have blocked had the server socket passed to it been a traditional (i.e. blocking) socket descriptor.read
, or write
, it'll return as quickly as possible without waiting.
read
is called, then some or all of those bytes are written into the supplied character buffer, and the number of bytes placed is returned.read
will return -1, provided the descriptor has been configured to be nonblocking. Again, this -1 normally denotes that some error occurred, but in this case, errno is set to EWOULDBLOCK. That's our clue that read didn't really fail. The -1/EWOULDBLOCK
combination is just saying that the call to read
would have blocked had the descriptor been a traditional (i.e. blocking) one.read
is called it's clear there'll never be any more data, read will return 0 as it has all along.write
system call as well, though it's rare for write
to return -1 unless a genuine error occurred, even if the supplied descriptor is nonblocking.slow-alphabet-server
implementation:static const string kAlphabet = "abcdefghijklmnopqrstuvwxyz";
static const useconds_t kDelay = 100000; // 100000 microseconds is 100 ms is 0.1 second
static void handleRequest(int client) {
sockbuf sb(client);
iosockstream ss(&sb);
for (size_t i = 0; i < kAlphabet.size(); i++) {
ss << kAlphabet[i] << flush;
usleep(kDelay); // 100000 microseconds is 100 ms is 0.1 seconds
}
}
static const unsigned short kSlowAlphabetServerPort = 41411;
int main(int argc, char *argv[]) {
int server = createServerSocket(kSlowAlphabetServerPort);
ThreadPool pool(128);
while (true) {
int client = accept(server, NULL, NULL);
pool.schedule([client]() { handleRequest(client); });
}
return 0;
}
slow-alphabet-server
operates much like time-server-concurrent
does.ThreadPool
sockbuf
is destroyed).slow-alphabet-server
, but it's intentionally slow to emulate the time a real server might take to synthesize a full response.
slow-alphabet-server
:int main(int argc, char *argv[]) {
int client = createClientSocket("localhost", kSlowAlphabetServerPort);
size_t numSuccessfulReads = 0;
size_t numBytes = 0;
while (true) {
char ch;
ssize_t count = read(client, &ch, 1);
if (count == 0) break; // we are truly done
numSuccessfulReads++;
numBytes += count;
cout << ch << flush;
}
close(client);
cout << endl;
cout << "Alphabet Length: " << numBytes << " bytes." << endl;
cout << "Num reads: " << numSuccessfulReads << endl;
return 0;
}
createClientSocket
.myth57:$ ./slow-alphabet-server &
[1] 7516
myth57:$ ./blocking-alphabet-client
abcdefghijklmnopqrstuvwxyz
Alphabet Length: 26 bytes.
Num reads: 26
myth57:$ time ./blocking-alphabet-client
abcdefghijklmnopqrstuvwxyz
Alphabet Length: 26 bytes.
Num reads: 26
real 0m2.609s
user 0m0.004s
sys 0m0.000s
myth57:$ kill -KILL 7516
[1] Killed ./slow-alphabet-server
static const unsigned short kSlowAlphabetServerPort = 41411;
int main(int argc, char *argv[]) {
int client = createClientSocket("localhost", kSlowAlphabetServerPort);
setAsNonBlocking(client);
size_t numReads = 0, numSuccessfulReads = 0, numUnsuccessfulReads = 0, numBytes = 0;
while (true) {
char ch;
ssize_t count = read(client, &ch, 1);
if (count == 0) break;
numReads++;
if (count > 0) {
numSuccessfulReads++;
numBytes += count;
cout << ch << flush;
} else {
assert(errno == EWOULDBLOCK || errno == EAGAIN);
numUnsuccessfulReads++;
}
}
close(client);
cout << endl;
cout << "Alphabet Length: " << numBytes << " bytes." << endl;
cout << "Num reads: " << numReads
<< " (" << numSuccessfulReads << " successful, " << numUnsuccessfulReads << " unsuccessful)." << endl;
return 0;
}
read
is incapable of blocking.
ch
to be updated and for the read
call to return 1.read
return a 0.errno
to be set to EWOULDBLOCK
myth57:$ ./slow-alphabet-server &
[1] 9801
myth57:$ ./non-blocking-alphabet-client
abcdefghijklmnopqrstuvwxyz
Alphabet Length: 26 bytes.
Num reads: 11394589 (26 successful, 11394563 unsuccessful).
myth57:$ time ./non-blocking-alphabet-client
abcdefghijklmnopqrstuvwxyz
Alphabet Length: 26 bytes.
Num reads: 11268990 (26 successful, 11268964 unsuccessful).
real 0m2.607s
user 0m0.264s
sys 0m2.340s
myth57:$ kill -KILL 9801
[1] Killed ./slow-alphabet-server myth57:$
OutboundFile
class is designed to read a local file and push its contents out over a supplied descriptor (and to do so without ever blocking).
OutboundFile
class.initialize
supplies the local file that should be used as a data source and the descriptor where that file's contents should be replicated.sendMoreData
pushes as much data as possible to the supplied sink, without blocking. It returns true
if it's at all possible there's more payload to be sent, and false
if all data has been fully pushed out. The fully documented interface file is right here.class OutboundFile {
public:
OutboundFile();
void initialize(const std::string& source, int sink);
bool sendMoreData();
private:
// implementation details omitted for the moment
}
/**
* File: outbound-file-test.cc
* --------------------------
* Demonstrates how one should use the OutboundFile class
* and can be used to confirm that it works properly.
*/
#include "outbound-file.h"
#include <unistd.h>
int main(int argc, char *argv[]) {
OutboundFile obf;
obf.initialize("outbound-file-test.cc", STDOUT_FILENO);
while (obf.sendMoreData()) {;}
return 0;
}
OutboundFile
class implementation works as expected.
// expensive-server.html is expensive because it's always using the CPU, even when there's nothing to do
static const unsigned short kDefaultPort = 12345;
static const string kFileToServe("expensive-server.cc.html");
int main(int argc, char **argv) {
int server = createServerSocket(kDefaultPort);
assert(server != kServerSocketFailure);
setAsNonBlocking(server);
cout << "Static file server listening on port " << kDefaultPort << "." << endl;
accept
. The return is immediate, because server has been configured to be nonblocking.accept
returns a -1, we verify the -1 isn't something to be concerned about.accept
surfaces a new connection , we create an new OutboundFile
on its behalf and append it to the running outboundFiles
list of clients currently being served.list<OutboundFile> outboundFiles;
while (true) {
// part 1: below
int client = accept(server, NULL, NULL);
if (client == -1) {
assert(errno == EWOULDBLOCK); // sanitycheck to confirm -1 doesn't represent a true failure
} else {
OutboundFile obf;
obf.initialize(kFileToServe, client);
outboundFiles.push_back(obf);
}
// part 2: presented on next slide
outboundFiles
list.OutboundFile
in the list and attempts to send some or all available data out to the client.
++iter
.outboundFiles
before advancing. (Fortunately, erase
does precisely what we want, and it returns the iterator addressing the next OutboundFile
in the list.) list<OutboundFile> outboundFiles;
while (true) {
// part 1: presented and discussed on previous slide
// part 2: below
auto iter = outboundFiles.begin();
while (iter != outboundFiles.end()) {
if (iter->sendMoreData()) ++iter;
else iter = outboundFiles.erase(iter);
}
}
}
setAsNonblocking
is fairly low-level.
fcntl
to do surgery on the relevant file session in the open file table.setAsNonblocking
and a few peer functions are presented below.void setAsNonBlocking(int descriptor) {
fcntl(descriptor, F_SETFL, fcntl(descriptor, F_GETFL) | O_NONBLOCK); // preserve other set flags
}
void setAsBlocking(int descriptor) {
fcntl(descriptor, F_SETFL, fcntl(descriptor, F_GETFL) & ~O_NONBLOCK); // suppress blocking bit, preserve others
}
bool isNonBlocking(int descriptor) {
return !isBlocking(descriptor);
}
bool isBlocking(int descriptor) {
return (fcntl(descriptor, F_GETFL) & O_NONBLOCK) == 0;
}
OutboundFile
abstraction without understanding how it works behind the scenes.
OutboundFile
implementation is accessible to someone just finishing up CS110.source
and sink
are nonblocking descriptors bound to the data source and recipientbuffer
is a reasonably sized character array that helps shovel bytes lifted from source via read
calls over to the sink
via write
calls.numBytesAvailable
stores the number of meaningful characters in buffer
.numBytesSent
tracks the portion of buffer
that's been pushed to the recipient.isSending
tracks whether all data has been pulled from source
and pushed to sink
.class OutboundFile {
public:
OutboundFile();
void initialize(const std::string& source, int sink);
bool sendMoreData();
private:
int source, sink;
static const size_t kBufferSize = 128;
char buffer[kBufferSize];
size_t numBytesAvailable, numBytesSent;
bool isSending;
// private helper methods discussed later
};
source
is a nonblocking file descriptor bound to some local fileO_RDONLY
), and the descriptor is configured to be nonblocking (O_NONBLOCK
) right from the start.read
calls.sink
is explicitly converted to be nonblocking, since it might be blocking, and sink
will very often be a socket descriptor that really should be nonblocking.OutboundFile::OutboundFile() : isSending(false) {}
void OutboundFile::initialize(const string& source, int sink) {
this->source = open(source.c_str(), O_RDONLY | O_NONBLOCK);
this->sink = sink;
setAsNonBlocking(this->sink);
numBytesAvailable = numBytesSent = 0;
isSending = true;
}
source
and written to sink
, and if so, it returns true
unless it further confirms all of data written to sink
has arrived at final destination, in which case it returns false
to state that syndication is complete.dataReadyToBeSent
checks to see if buffer
houses data yet to be pushed out. If not, then it attempts to readMoreData
. If after reading more data the buffer is still empty—that is, a single call to read
resulted in a -1/EWOULDBLOCK
pair, then we return true
as a statement that there's no data to be written, no need to try, but come back later to see if that changes.writeMoreData
is an opportunity to push data out to sink.bool OutboundFile::sendMoreData() {
if (!isSending) return !allDataFlushed();
if (!dataReadyToBeSent()) {
readMoreData();
if (!dataReadyToBeSent()) return true;
}
writeMoreData();
return true;
}
expensive-server.cc
efficient-server.cc
expensive-server.cc
"Illustrates how nonblocking IO can be used to implement a single-threaded web server. This particular example wastes a huge amount of CPU time (as it while loops forever without blocking), but it does demonstrate how nonblocking IO can be used to very easily serve multiple client requests at a time without threads or multiple processes."expensive-server.cc
can be found here.accept
:static const unsigned short kDefaultPort = 12345;
static const string kFileToServe("expensive-server.cc.html");
int main(int argc, char **argv) {
int serverSocket = createServerSocket(kDefaultPort);
if (serverSocket == kServerSocketFailure) {
cerr << "Could not start server. Port " << kDefaultPort << " is probably in use." << endl;
return 0;
}
setAsNonBlocking(serverSocket);
cout << "Static file server listening on port " << kDefaultPort << "." << endl;
list<OutboundFile> outboundFiles;
size_t numConnections = 0;
size_t numActiveConnections = 0;
size_t numAcceptCalls = 0;
while (true) {
// right here!
int clientSocket = accept(serverSocket, NULL, NULL);
cout << "Num calls to accept: " << ++numAcceptCalls << "." << endl;
if (clientSocket == -1) {
assert(errno == EWOULDBLOCK);
} else {
OutboundFile obf;
obf.initialize(kFileToServe, clientSocket);
outboundFiles.push_back(obf);
cout << "Connection #" << ++numConnections << endl;
cout << "Queue size: " << ++numActiveConnections << endl;
}
auto iter = outboundFiles.begin();
while (iter != outboundFiles.end()) {
if (iter->sendMoreData()) {
++iter;
} else {
iter = outboundFiles.erase(iter);
cout << "Queue size: " << --numActiveConnections << endl;
}
}
}
}
$ cat oneSecond.sh
#!/bin/bash
./expensive-server &
pid=$!
sleep 1
kill $pid
$ ./oneSecond
Static file server listening on port 12345.
Num calls to accept: 1.
Num calls to accept: 2.
Num calls to accept: 3.
Num calls to accept: 4.
Num calls to accept: 5.
Num calls to accept: 6.
... lots of lines removed
Num calls to accept: 196716.
Num calls to accept: 196717.
Num calls to accept: 196718.
Num calls to accept: 196719.
Num calls to accept: 196720.
Num calls to accept: 196721.
$
int epoll_create1(int flags);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
struct epoll_event {
uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
typedef union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
static const unsigned short kDefaultPort = 33333;
int main(int argc, char **argv) {
int server = createServerSocket(kDefaultPort);
if (server == kServerSocketFailure) {
cerr << "Failed to start server. Port " << kDefaultPort << " is probably already in use." << endl;
return 1;
}
cout << "Server listening on port " << kDefaultPort << endl;
runServer(server);
return 0;
}
static void runServer(int server) {
setAsNonBlocking(server);
int ws = buildInitialWatchSet(server);
static const int kMaxEvents = 64;
static int buildInitialWatchSet(int server) {
int ws = epoll_create1(0);
struct epoll_event info = {.events = EPOLLIN | EPOLLET, .data = {.fd = server}};
epoll_ctl(ws, EPOLL_CTL_ADD, server, &info);
return ws;
}
struct epoll_event events[kMaxEvents];
while (true) {
int numEvents = epoll_wait(ws, events, kMaxEvents, /* timeout = */ -1);
for (int i = 0; i < numEvents; i++) {
if (events[i].data.fd == server) {
acceptNewConnections(ws, server);
}
} else if (events[i].events & EPOLLIN) { // we're still reading the client's request
consumeAvailableData(ws, events[i].data.fd);
}
} else { // events[i].events & EPOLLOUT
publishResponse(events[i].data.fd);
}
}
}
}
static void acceptNewConnections(int ws, int server) {
while (true) {
int clientSocket = accept4(server, NULL, NULL, SOCK_NONBLOCK);
if (clientSocket == -1) return;
struct epoll_event info = {.events = EPOLLIN | EPOLLET, .data = {.fd = clientSocket}};
epoll_ctl(ws, EPOLL_CTL_ADD, clientSocket, &info);
}
}
static const size_t kBufferSize = 1;
static const string kRequestHeaderEnding("\r\n\r\n");
static void consumeAvailableData(int ws, int client) {
static map<int, string> requests; // tracks what's been read in thus far over each client socket
size_t pos = string::npos;
while (pos == string::npos) {
char buffer[kBufferSize];
ssize_t count = read(client, buffer, kBufferSize);
if (count == -1 && errno == EWOULDBLOCK) return; // not done reading everything yet, so return
if (count <= 0) { close(client); break; } // passes? then bail on connection, as it's borked
requests[client] += string(buffer, buffer + count);
pos = requests[client].find(kRequestHeaderEnding);
if (pos == string::npos) continue;
cout << "Num Active Connections: " << requests.size() << endl;
cout << requests[client].substr(0, pos + kRequestHeaderEnding.size()) << flush;
struct epoll_event info = {.events = EPOLLOUT | EPOLLET, .data = {.fd = client}};
epoll_ctl(ws, EPOLL_CTL_MOD, client, &info); // MOD == modify existing event
}
requests.erase(client);
}
static const string kResponseString("HTTP/1.1 200 OK\r\n\r\n"
"<b>Thank you for your request! We're working on it! No, really!</b><br/>"
"<br/><img src=\"http://vignette3.wikia.nocookie.net/p__/images/e/e0/"
"Agnes_Unicorn.png/revision/latest?cb=20160221214120&path-prefix=protagonist\"/>");
static void publishResponse(int client) {
static map<int, size_t> responses;
responses[client]; // insert a 0 if key isn't present
while (responses[client] < kResponseString.size()) {
ssize_t count = write(client, kResponseString.c_str() + responses[client],
kResponseString.size() - responses[client]);
if (count == -1 && errno == EAGAIN) return;
if (count == -1) break;
assert(count > 0);
responses[client] += count;
}
responses.erase(client);
close(client);
}