470 lines
26 KiB
HTML
470 lines
26 KiB
HTML
<!DOCTYPE html>
|
|
|
|
|
|
<html class="no-js" lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<title>Async IO on Linux: select, poll, and epoll</title>
|
|
<meta name="author" content="Julia Evans">
|
|
<meta name="HandheldFriendly" content="True">
|
|
<meta name="MobileOptimized" content="320">
|
|
<meta name="description" content="Async IO on Linux: select, poll, and epoll">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
|
|
<meta property="og:title" content='Async IO on Linux: select, poll, and epoll'>
|
|
<meta property="og:type" content="website" />
|
|
<meta property="og:url" content="https://jvns.ca/blog/2017/06/03/async-io-on-linux--select--poll--and-epoll/" />
|
|
<meta property="og:site_name" content="Julia Evans" />
|
|
|
|
<link rel="canonical" href="https://jvns.ca/blog/2017/06/03/async-io-on-linux--select--poll--and-epoll/">
|
|
<link href="/favicon.ico" rel="icon">
|
|
|
|
<link href="/stylesheets/screen.css" rel="preload" type="text/css" as="style">
|
|
|
|
<link href="/stylesheets/screen.css" media="screen, projection" rel="stylesheet" type="text/css">
|
|
<link href="/stylesheets/print.css" media="print" rel="stylesheet" type="text/css">
|
|
|
|
|
|
<link href="/atom.xml" rel="alternate" title="Julia Evans" type="application/atom+xml">
|
|
|
|
|
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.4/dist/katex.min.css" integrity="sha384-vKruj+a13U8yHIkAyGgK1J3ArTLzrFGBbBc0tDp4ad/EyewESeXE/Iv67Aj8gKZ0" crossorigin="anonymous">
|
|
<script defer data-domain="jvns.ca" src="https://plausible.io/js/script.js"></script>
|
|
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.4/dist/katex.min.js" integrity="sha384-PwRUT/YqbnEjkZO0zZxNqcxACrXe+j766U2amXcgMg5457rve2Y7I6ZJSm2A0mS4" crossorigin="anonymous"></script>
|
|
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.4/dist/contrib/auto-render.min.js" integrity="sha384-+VBxd3r6XgURycqtZ117nYw44OOcIax56Z4dCRWbxyPt0Koah1uHoK0o4+/RRE05" crossorigin="anonymous" onload="renderMathInElement(document.body);"></script>
|
|
|
|
<script defer type="text/javascript">
|
|
window.heap=window.heap||[],heap.load=function(e,t){window.heap.appid=e,window.heap.config=t=t||{};var r=document.createElement("script");r.type="text/javascript",r.async=!0,r.src="https://cdn.heapanalytics.com/js/heap-"+e+".js";var a=document.getElementsByTagName("script")[0];a.parentNode.insertBefore(r,a);for(var n=function(e){return function(){heap.push([e].concat(Array.prototype.slice.call(arguments,0)))}},p=["addEventProperties","addUserProperties","clearEventProperties","identify","resetIdentity","removeEventProperty","setEventProperties","track","unsetEventProperty"],o=0;o<p.length;o++)heap[p[o]]=n(p[o])};
|
|
heap.load("2242143965");
|
|
</script>
|
|
</head>
|
|
<body>
|
|
<div id="skiptocontent">
|
|
<a href="#main">Skip to main content</a>
|
|
</div>
|
|
<div id="wrap">
|
|
<header role="banner">
|
|
<hgroup>
|
|
<h1><a href="/">Julia Evans</a></h1>
|
|
</hgroup>
|
|
<ul class="header-links">
|
|
<li><a href="/about">About</a></li>
|
|
<li><a href="/talks">Talks</a></li>
|
|
<li><a href="/projects/">Projects</a></li>
|
|
<li><a rel="me" href="https://social.jvns.ca/@b0rk">Mastodon</a></li>
|
|
<li><a href="https://bsky.app/profile/b0rk.jvns.ca">Bluesky</a></li>
|
|
<li><a href="https://github.com/jvns">Github</a></li>
|
|
</ul>
|
|
</header>
|
|
<nav role="navigation" class="header-nav"><ul class="main-navigation">
|
|
<li><a href="/categories/favorite/">Favorites</a></li>
|
|
<li><a href="/til/">TIL</a></li>
|
|
<li><a href="https://wizardzines.com">Zines</a></li>
|
|
<li class="subscription" data-subscription="rss"><a href="/atom.xml" rel="subscribe-rss" title="subscribe via RSS">RSS</a></li>
|
|
</ul>
|
|
</nav>
|
|
<div id="main">
|
|
<div id="content">
|
|
|
|
|
|
<div>
|
|
<article class="hentry" role="article">
|
|
<header>
|
|
<h1 class="entry-title">Async IO on Linux: select, poll, and epoll</h1>
|
|
|
|
<div class="post-tags">
|
|
|
|
</div>
|
|
<p class="meta sans">
|
|
<time class="date" datetime="2017-06-03T12:28:36" pubdate data-updated="true">
|
|
|
|
June 3, 2017
|
|
|
|
</time>
|
|
</p>
|
|
</header>
|
|
<main>
|
|
<p>This week I got a new book in the mail: <a href="https://www.nostarch.com/tlpi">The Linux Programming Interface</a>. My awesome coworker Arshia recommended it to me so I bought it! It’s written by the
|
|
maintainer of the <a href="https://www.kernel.org/doc/man-pages/">Linux man-pages project</a>, Michael Kerrisk. It talks about the Linux programming interface as of kernel 2.6.x.</p>
|
|
<p>Here’s the cover.</p>
|
|
<div align="center">
|
|
<img src="https://jvns.ca/images/linuxprogramming.png" width=200px>
|
|
</div>
|
|
<p>In the contributing guidelines (you can contribute to the linux man pages!! mind=blown), there’s a list of <a href="https://www.kernel.org/doc/man-pages/missing_pages.html">missing man pages</a> that would be useful to contribute. It says:</p>
|
|
<blockquote>
|
|
<p>You need to have a reasonably high degree of understanding of the
|
|
topic, or be prepared to invest the time (e.g., reading source code,
|
|
writing test programs) to gain that understanding. Writing test
|
|
programs is important: quite a few kernel and glibc bugs have been
|
|
uncovered while writing test programs during the preparation of man
|
|
pages.</p>
|
|
</blockquote>
|
|
<p>I thought this was a cool reminder of how you can learn a lot by
|
|
documenting something & writing small test programs!</p>
|
|
<p>But today we’re going to talk about something I learned from this book:
|
|
the <code>select</code>, <code>poll</code>, and <code>epoll</code> system calls.</p>
|
|
<h3 id="chapter-63-alternative-i-o-models" class="post-heading">
|
|
<a href="#chapter-63-alternative-i-o-models">
|
|
Chapter 63: Alternative I/O models
|
|
</a>
|
|
</h3>
|
|
<p>This book is huge: 1400 pages. I started it at Chapter 63 (“alternative
|
|
I/O models”) because I’ve been meaning to understand better what’s up with
|
|
<code>select</code>, <code>poll</code> and <code>epoll</code> for quite some time. And writing up things
|
|
I learn helps me understand them, so here’s my attempt at explaining!</p>
|
|
<p>This chapter is basically about how to monitor a lot of file descriptors
|
|
for new input/output. Who needs to watch a lot of file descriptors at a
|
|
time? Servers!</p>
|
|
<p>For example if you’re writing a web server in node.js on Linux, it’s
|
|
actually using the <code>epoll</code> Linux system call under the hood. Let’s talk
|
|
about why, how <code>epoll</code> is different from <code>poll</code> and <code>select</code>, and about how it works!</p>
|
|
<h3 id="servers-need-to-watch-a-lot-of-file-descriptors" class="post-heading">
|
|
<a href="#servers-need-to-watch-a-lot-of-file-descriptors">
|
|
Servers need to watch a lot of file descriptors
|
|
</a>
|
|
</h3>
|
|
<p>Suppose you’re a webserver. Every
|
|
time you accept a connection with the <code>accept</code> system call (<a href="http://man7.org/linux/man-pages/man2/accept.2.html">here’s the man page</a>),
|
|
you get a new file descriptor representing that connection.</p>
|
|
<p>If you’re a web server, you might have thousands of connections open at
|
|
the same time. You need to know when people send you new data on those
|
|
connections, so you can process and respond to them.</p>
|
|
<p>You could have a loop that basically does:</p>
|
|
<pre><code>for x in open_connections:
|
|
if has_new_input(x):
|
|
process_input(x)
|
|
</code></pre>
|
|
<p>The problem with this is that it can waste a lot of CPU time. Instead of
|
|
spending all CPU time to ask “are there updates now? how about now? how
|
|
about now? how about now?”, instead we’d rather just ask the Linux kernel
|
|
“hey, here are 100 file descriptors. Tell me when one of them is
|
|
updated!”.</p>
|
|
<p>The 3 system calls that let you ask Linux to monitor lots of file
|
|
descriptors are <code>poll</code>, <code>epoll</code> and <code>select</code>. Let’s start with poll and
|
|
select because that’s where the chapter started.</p>
|
|
<h3 id="first-way-select-poll" class="post-heading">
|
|
<a href="#first-way-select-poll">
|
|
First way: select & poll
|
|
</a>
|
|
</h3>
|
|
<p>These 2 system calls are available on any Unix system, while <code>epoll</code> is
|
|
Linux-specific. Here’s basically how they work:</p>
|
|
<ol>
|
|
<li>Give them a list of file descriptors to get information about</li>
|
|
<li>They tell you which ones have data available to read/write to</li>
|
|
</ol>
|
|
<p>The first surprising thing I learned from this chapter are that <strong>poll
|
|
and select fundamentally use the same code</strong>.</p>
|
|
<p>I went to look at the definition of <code>poll</code> and <code>select</code> in the Linux kernel
|
|
source to confirm this and it’s true!</p>
|
|
<ul>
|
|
<li>here’s the <a href="https://github.com/torvalds/linux/blob/v4.10/fs/select.c#L634-L656">definition of the select syscall</a> and <a href="https://github.com/torvalds/linux/blob/v4.10/fs/select.c#L404-L542">do_select</a></li>
|
|
<li>and the <a href="https://github.com/torvalds/linux/blob/v4.10/fs/select.c#L1005-L1055">definition of the poll syscall</a> and <a href="https://github.com/torvalds/linux/blob/v4.10/fs/select.c#L795-L879">do_poll</a></li>
|
|
</ul>
|
|
<p>They both call a lot of the same functions. One thing that the book
|
|
mentioned in particular is that <code>poll</code> returns a larger set of possible
|
|
results for file descriptors like <code>POLLRDNORM | POLLRDBAND | POLLIN | POLLHUP | POLLERR</code> while <code>select</code> just tells you “there’s input / there’s output / there’s an error”.</p>
|
|
<p><code>select</code> translates from <code>poll</code>’s more detailed results (like <code>POLLWRBAND</code>) into a general “you can write”. You can see the code where it does this in Linux 4.10 <a href="https://github.com/torvalds/linux/blob/v4.10/fs/select.c#L468-L482">here</a>.</p>
|
|
<p>The next thing I learned is that <strong>poll can perform better than select if you have a sparse set of file descriptors</strong> .</p>
|
|
<p>To see this, you can actually just look at the signatures for poll and
|
|
select!</p>
|
|
<pre><code>int ppoll(struct pollfd *fds, nfds_t nfds,
|
|
const struct timespec *tmo_p, const sigset_t
|
|
*sigmask)`
|
|
int pselect(int nfds, fd_set *readfds, fd_set *writefds,
|
|
fd_set *exceptfds, const struct timespec *timeout,
|
|
const sigset_t *sigmask);
|
|
</code></pre>
|
|
<p>With <code>poll</code>, you tell it “here are the file descriptors I want to monitor: 1,
|
|
3, 8, 19, etc” (that’s the <code>pollfd</code> argument. With select, you tell it “I want to monitor 19 file
|
|
descriptors. Here are 3 bitsets with which ones to monitor for reads / writes / exceptions.”
|
|
So when it runs, it <a href="https://github.com/torvalds/linux/blob/v4.10/fs/select.c#L440">loops from 0 to 19 file descriptors</a>,
|
|
even if you were actually only interested in 4 of them.</p>
|
|
<p>There are a lot more specific details about how <code>poll</code> and <code>select</code> are
|
|
different in the chapter but those were the 2 main things I learned!</p>
|
|
<h3 id="why-don-t-we-use-poll-and-select" class="post-heading">
|
|
<a href="#why-don-t-we-use-poll-and-select">
|
|
why don’t we use poll and select?
|
|
</a>
|
|
</h3>
|
|
<p>Okay, but on Linux we said that your node.js server won’t use either poll or
|
|
select, it’s going to use <code>epoll</code>. Why?</p>
|
|
<p>From the book:</p>
|
|
<blockquote>
|
|
<p>On each call to <code>select()</code> or <code>poll()</code>, the kernel must check all of the
|
|
specified file descriptors to see if they are ready. When monitoring a large
|
|
number of file descriptors that are in a densely packed range, the timed
|
|
required for this operation greatly outweighs [the rest of the stuff they have
|
|
to do]</p>
|
|
</blockquote>
|
|
<p>Basically: every time you call <code>select</code> or <code>poll</code>, the kernel needs to
|
|
check from scratch whether your file descriptors are available for
|
|
writing. The kernel doesn’t remember the list of file descriptors it’s
|
|
supposed to be monitoring!</p>
|
|
<h3 id="signal-driven-i-o-is-this-a-thing-people-use" class="post-heading">
|
|
<a href="#signal-driven-i-o-is-this-a-thing-people-use">
|
|
Signal-driven I/O (is this a thing people use?)
|
|
</a>
|
|
</h3>
|
|
<p>The book actually describes 2 ways to ask the kernel to remember the
|
|
list of file descriptors it’s supposed to be monitoring: signal-drive
|
|
I/O and <code>epoll</code>. Signal-driven I/O is a way to get the kernel to send
|
|
you a signal when a file descriptor is updated by calling <code>fcntl</code>. I’ve
|
|
never heard of anyone using this and the book makes it sound like
|
|
<code>epoll</code> is just better so we’re going to ignore it for now and talk about epoll.</p>
|
|
<h3 id="level-triggered-vs-edge-triggered" class="post-heading">
|
|
<a href="#level-triggered-vs-edge-triggered">
|
|
level-triggered vs edge-triggered
|
|
</a>
|
|
</h3>
|
|
<p>Before we talk about epoll, we need to talk about “level-triggered” vs
|
|
“edge-triggered” notifications about file descriptors. I’d never heard
|
|
this terminology before (I think it comes from electrical engineering
|
|
maybe?). Basically there are 2 ways to get notifications</p>
|
|
<ul>
|
|
<li>get a list of every file descriptor you’re interested in that is readable (“level-triggered”)</li>
|
|
<li>get notifications every time a file descriptor becomes readable
|
|
(“edge-triggered”)</li>
|
|
</ul>
|
|
<h3 id="what-s-epoll" class="post-heading">
|
|
<a href="#what-s-epoll">
|
|
what’s epoll?
|
|
</a>
|
|
</h3>
|
|
<p>Okay, we’re ready to talk about epoll!! This is very exciting to because
|
|
I’ve seen <code>epoll_wait</code> a lot when stracing programs and I often feel
|
|
kind of fuzzy about what it means exactly.</p>
|
|
<p>The <code>epoll</code> group of system calls (<code>epoll_create</code>, <code>epoll_ctl</code>,
|
|
<code>epoll_wait</code>) give the Linux kernel a list of file descriptors to track
|
|
and ask for updates about activity on those file descriptors.</p>
|
|
<p>Here are the steps to using epoll:</p>
|
|
<ol>
|
|
<li>Call <code>epoll_create</code> to tell the kernel you’re gong to be epolling! It
|
|
gives you an id back</li>
|
|
<li>Call <code>epoll_ctl</code> to tell the kernel file descriptors you’re
|
|
interested in updates about. Interestingly, you can give it lots of
|
|
different kinds of file descriptors (pipes,
|
|
FIFOs, sockets, POSIX message queues, inotify instances, devices, & more), but
|
|
<strong>not regular files</strong>. I think this makes sense – pipes & sockets
|
|
have a pretty simple API (one process writes to the pipe, and another
|
|
process reads!), so it makes sense to say “this pipe has new data for
|
|
reading”. But files are weird! You can write to the middle of a file!
|
|
So it doesn’t really make sense to say “there’s new data available
|
|
for reading in this file”.</li>
|
|
<li>Call <code>epoll_wait</code> to wait for updates about the list of files
|
|
you’re interested in.</li>
|
|
</ol>
|
|
<h3 id="performance-select-poll-vs-epoll" class="post-heading">
|
|
<a href="#performance-select-poll-vs-epoll">
|
|
performance: select & poll vs epoll
|
|
</a>
|
|
</h3>
|
|
<p>In the book there’s a table comparing the performance for 100,000
|
|
monitoring operations:</p>
|
|
<pre><code># operations | poll | select | epoll
|
|
10 | 0.61 | 0.73 | 0.41
|
|
100 | 2.9 | 3.0 | 0.42
|
|
1000 | 35 | 35 | 0.53
|
|
10000 | 990 | 930 | 0.66
|
|
</code></pre>
|
|
<p>So using epoll really is a lot faster once you have more than 10 or so
|
|
file descriptors to monitor.</p>
|
|
<h3 id="who-uses-epoll" class="post-heading">
|
|
<a href="#who-uses-epoll">
|
|
who uses epoll?
|
|
</a>
|
|
</h3>
|
|
<p>I sometimes see <code>epoll_wait</code> when I strace a program. Why? There is the
|
|
kind of obvious but unhelpful answer “it’s monitoring some file
|
|
descriptors”, but we can do better!</p>
|
|
<p>First – if you’re using green threads or an event loop, you’re likely
|
|
using epoll to do all your networking & pipe I/O!</p>
|
|
<p>For example, here’s a golang program that uses epoll on Linux!</p>
|
|
<pre><code>package main
|
|
|
|
import "net/http"
|
|
import "io/ioutil"
|
|
|
|
func main() {
|
|
resp, err := http.Get("http://example.com/")
|
|
if err != nil {
|
|
// handle error
|
|
}
|
|
defer resp.Body.Close()
|
|
_, err = ioutil.ReadAll(resp.Body)
|
|
}
|
|
</code></pre>
|
|
<p>Here you can see the golang run time using epoll to do a DNS lookup:</p>
|
|
<pre><code>16016 connect(3, {sa_family=AF_INET, sin_port=htons(53), sin_addr=inet_addr("127.0.1.1")}, 16 <unfinished ...>
|
|
16020 socket(PF_INET, SOCK_DGRAM|SOCK_CLOEXEC|SOCK_NONBLOCK, IPPROTO_IP
|
|
16016 epoll_create1(EPOLL_CLOEXEC <unfinished ...>
|
|
16016 epoll_ctl(5, EPOLL_CTL_ADD, 3, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=334042824, u64=139818699396808}}
|
|
16020 connect(4, {sa_family=AF_INET, sin_port=htons(53), sin_addr=inet_addr("127.0.1.1")}, 16 <unfinished ...>
|
|
16020 epoll_ctl(5, EPOLL_CTL_ADD, 4, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=334042632, u64=139818699396616}}
|
|
</code></pre>
|
|
<p>Basically what this is doing is connecting 2 sockets (on file
|
|
descriptors 3 and 4) to make DNS queries (to 127.0.1.1:53), and then
|
|
using <code>epoll_ctl</code> to ask epoll to give us updates about them</p>
|
|
<p>Then it makes 2 DNS queries for example.com (why 2? nelhage suggests one
|
|
of them is querying for the A record, and one for the AAAA record!), and
|
|
uses <code>epoll_wait</code> to wait for replies</p>
|
|
<pre><code># these are DNS queries for example.com!
|
|
16016 write(3, "\3048\1\0\0\1\0\0\0\0\0\0\7example\3com\0\0\34\0\1", 29
|
|
16020 write(4, ";\251\1\0\0\1\0\0\0\0\0\0\7example\3com\0\0\1\0\1", 29
|
|
# here it tries to read a response but I guess there's no response
|
|
# available yet
|
|
16016 read(3, <unfinished ...>
|
|
16020 read(4, <unfinished ...>
|
|
16016 <... read resumed> 0xc8200f4000, 512) = -1 EAGAIN (Resource temporarily unavailable)
|
|
16020 <... read resumed> 0xc8200f6000, 512) = -1 EAGAIN (Resource temporarily unavailable)
|
|
# then it uses epoll to wait for responses
|
|
16016 epoll_wait(5, <unfinished ...>
|
|
16020 epoll_wait(5, <unfinished ...>
|
|
</code></pre>
|
|
<p>So one reason your program might be using epoll “it’s in Go / node.js /
|
|
Python with gevent and it’s doing networking”.</p>
|
|
<p>What libraries do go/node.js/Python use to use epoll?</p>
|
|
<ul>
|
|
<li>node.js uses <a href="https://github.com/libuv/libuv">libuv</a> (which was
|
|
written for the node.js project)</li>
|
|
<li>the gevent networking library in Python uses <a href="https://blog.gevent.org/2011/04/28/libev-and-libevent/">libev/libevent</a></li>
|
|
<li>golang uses some custom code, because it’s Go. This <a href="https://github.com/golang/go/blob/91c9b0d568e41449f26858d88eb2fd085eaf306d/src/runtime/netpoll_epoll.go">looks like it might be the implementation of network polling with epoll in the golang runtime</a> – it’s only about 100 lines which is interesting. You can see the general netpoll interface <a href="https://golang.org/src/runtime/netpoll.go">here</a> – it’s implemented on BSDs with kqueue instead</li>
|
|
</ul>
|
|
<p>Webservers also implement epoll – for example <a href="https://github.com/nginx/nginx/blob/0759f088a532ec48170ca03d694cc103757a0f4c/src/event/modules/ngx_epoll_module.c">here’s the epoll code in nginx</a>.</p>
|
|
<h3 id="more-select-epoll-reading" class="post-heading">
|
|
<a href="#more-select-epoll-reading">
|
|
more select & epoll reading
|
|
</a>
|
|
</h3>
|
|
<p>I liked these 3 posts by Marek:</p>
|
|
<ul>
|
|
<li><a href="https://idea.popcount.org/2017-01-06-select-is-fundamentally-broken/">select is fundamentally broken</a></li>
|
|
<li><a href="https://idea.popcount.org/2017-02-20-epoll-is-fundamentally-broken-12/">epoll is fundamentally broken part 1</a></li>
|
|
<li><a href="https://idea.popcount.org/2017-03-20-epoll-is-fundamentally-broken-22/">epoll is fundamentally broken part 2</a></li>
|
|
</ul>
|
|
<p>In particular these talk about how epoll’s support for multithreaded
|
|
programs has not historically been good, though there were some
|
|
improvements in Linux 4.5.</p>
|
|
<p>and this:</p>
|
|
<ul>
|
|
<li><a href="http://aivarsk.github.io/2017/04/06/select/">using select (2) the right way</a></li>
|
|
</ul>
|
|
<h3 id="ok-that-s-enough" class="post-heading">
|
|
<a href="#ok-that-s-enough">
|
|
ok that’s enough
|
|
</a>
|
|
</h3>
|
|
<p>I learned quite a few new things about select & epoll by writing this
|
|
post! We’re at 1800 words now so I think that’s enough. Looking forward
|
|
to reading more of this Linux programming interface book and finding out
|
|
more things!</p>
|
|
<p>Probably there are some wrong things in this post, let me know what they
|
|
are!</p>
|
|
<p>One small thing I like about my job is that I can expense programming
|
|
books! This is cool because sometimes it causes me to buy and read books
|
|
that teach me things that I might not have learned otherwise. And buying
|
|
a book is way cheaper than going to a conference!</p>
|
|
|
|
</main>
|
|
|
|
<footer>
|
|
|
|
<style type="text/css">
|
|
#mc_embed_signup{background:#fff; clear:left; font:14px Helvetica,Arial,sans-serif; display: inline;}
|
|
#mc_embed_signup {
|
|
display: inline;
|
|
}
|
|
#mc_embed_signup input.button {
|
|
background: #ff5e00;
|
|
display: inline;
|
|
color: white;
|
|
padding: 6px 12px;
|
|
}
|
|
</style>
|
|
<div class="sharing">
|
|
|
|
<style>
|
|
.form-inline {
|
|
display:flex; flex-flow: row wrap; justify-content: center;
|
|
}
|
|
.form-inline input, .form-inline span {
|
|
padding: 10px;
|
|
}
|
|
.form-inline input {
|
|
display:inline;
|
|
max-width:30%;
|
|
margin: 0 10px 0 0;
|
|
background-color: #fff;
|
|
border: 1px solid #ddd;
|
|
border-radius: 5px;
|
|
padding: 10px;
|
|
}
|
|
button {
|
|
background-color: #f50;
|
|
box-shadow: none;
|
|
border: 0;
|
|
border-radius: 5px;
|
|
color: white;
|
|
padding: 5px 10px;
|
|
}
|
|
@media (max-width: 800px) {
|
|
.form-inline input {
|
|
margin: 10px 0;
|
|
max-width:100% !important;
|
|
}
|
|
.form-inline {
|
|
flex-direction: column;
|
|
align-items: stretch;
|
|
}
|
|
}
|
|
</style>
|
|
|
|
<div align="center">
|
|
<form class="form-inline" action="https://app.convertkit.com/forms/1052396/subscriptions" method="post" data-uid="8884355abb" data-format="inline" data-version="5">
|
|
<span> Want a weekly digest of this blog?</span>
|
|
<input name="email_address" type="text" placeholder="Email address" />
|
|
<button type="submit" data-element="submit">Subscribe</button>
|
|
</form>
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
<p class="meta">
|
|
|
|
<a class="basic-alignment left" href="https://jvns.ca/blog/2017/05/14/handwritten-books/" title="Previous Post: Handwritten books">Handwritten books</a>
|
|
|
|
|
|
<a class="basic-alignment right" href="https://jvns.ca/blog/2017/06/04/learning-about-kubernetes/" title="Next Post: A few things I've learned about Kubernetes">A few things I've learned about Kubernetes</a>
|
|
|
|
</p>
|
|
</footer>
|
|
|
|
</article>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
<nav role="navigation" class="footer-nav"> <a href="/">Archives</a>
|
|
</nav>
|
|
<footer role="contentinfo"><span class="credit">© Julia Evans. </span>
|
|
<span>If you like this, you may like <a href="https://web.archive.org/web/20181228051203/http://www.uliaea.ca/">Ulia Ea</a> or, more seriously, this list of <a href="https://jvns.ca/blogroll">blogs I love</a> or some <a href="https://jvns.ca/bookshelf">books I've read</a>. <br>
|
|
<p class="rc-scout__text"><i class="rc-scout__logo"></i>
|
|
You might also like the <a class="rc-scout__link" href="https://www.recurse.com/scout/click?t=546ea46360584b522270b8c3e5d830f8">Recurse Center</a>, my very favorite programming community <a href="/categories/hackerschool/">(my posts about it)</a></p>
|
|
</span>
|
|
<style class="rc-scout__style" type="text/css">.rc-scout{display:block;padding:0;border:0;margin:0;}.rc-scout__text{display:block;padding:0;border:0;margin:0;height:100%;font-size:100%;}.rc-scout__logo{display:inline-block;padding:0;border:0;margin:0;width:0.85em;height:0.85em;background:no-repeat center url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2012%2015%22%3E%3Crect%20x%3D%220%22%20y%3D%220%22%20width%3D%2212%22%20height%3D%2210%22%20fill%3D%22%23000%22%3E%3C%2Frect%3E%3Crect%20x%3D%221%22%20y%3D%221%22%20width%3D%2210%22%20height%3D%228%22%20fill%3D%22%23fff%22%3E%3C%2Frect%3E%3Crect%20x%3D%222%22%20y%3D%222%22%20width%3D%228%22%20height%3D%226%22%20fill%3D%22%23000%22%3E%3C%2Frect%3E%3Crect%20x%3D%222%22%20y%3D%223%22%20width%3D%221%22%20height%3D%221%22%20fill%3D%22%2361ae24%22%3E%3C%2Frect%3E%3Crect%20x%3D%224%22%20y%3D%223%22%20width%3D%221%22%20height%3D%221%22%20fill%3D%22%2361ae24%22%3E%3C%2Frect%3E%3Crect%20x%3D%226%22%20y%3D%223%22%20width%3D%221%22%20height%3D%221%22%20fill%3D%22%2361ae24%22%3E%3C%2Frect%3E%3Crect%20x%3D%223%22%20y%3D%225%22%20width%3D%222%22%20height%3D%221%22%20fill%3D%22%2361ae24%22%3E%3C%2Frect%3E%3Crect%20x%3D%226%22%20y%3D%225%22%20width%3D%222%22%20height%3D%221%22%20fill%3D%22%2361ae24%22%3E%3C%2Frect%3E%3Crect%20x%3D%224%22%20y%3D%229%22%20width%3D%224%22%20height%3D%223%22%20fill%3D%22%23000%22%3E%3C%2Frect%3E%3Crect%20x%3D%221%22%20y%3D%2211%22%20width%3D%2210%22%20height%3D%224%22%20fill%3D%22%23000%22%3E%3C%2Frect%3E%3Crect%20x%3D%220%22%20y%3D%2212%22%20width%3D%2212%22%20height%3D%223%22%20fill%3D%22%23000%22%3E%3C%2Frect%3E%3Crect%20x%3D%222%22%20y%3D%2213%22%20width%3D%221%22%20height%3D%221%22%20fill%3D%22%23fff%22%3E%3C%2Frect%3E%3Crect%20x%3D%223%22%20y%3D%2212%22%20width%3D%221%22%20height%3D%221%22%20fill%3D%22%23fff%22%3E%3C%2Frect%3E%3Crect%20x%3D%224%22%20y%3D%2213%22%20width%3D%221%22%20height%3D%221%22%20fill%3D%22%23fff%22%3E%3C%2Frect%3E%3Crect%20x%3D%225%22%20y%3D%2212%22%20width%3D%221%22%20height%3D%221%22%20fill%3D%22%23fff%22%3E%3C%2Frect%3E%3Crect%20x%3D%226%22%20y%3D%2213%22%20width%3D%221%22%20height%3D%221%22%20fill%3D%22%23fff%22%3E%3C%2Frect%3E%3Crect%20x%3D%227%22%20y%3D%2212%22%20width%3D%221%22%20height%3D%221%22%20fill%3D%22%23fff%22%3E%3C%2Frect%3E%3Crect%20x%3D%228%22%20y%3D%2213%22%20width%3D%221%22%20height%3D%221%22%20fill%3D%22%23fff%22%3E%3C%2Frect%3E%3Crect%20x%3D%229%22%20y%3D%2212%22%20width%3D%221%22%20height%3D%221%22%20fill%3D%22%23fff%22%3E%3C%2Frect%3E%3C%2Fsvg%3E');}.rc-scout__link:link,.rc-scout__link:visited{color:#61ae24;text-decoration:underline;}.rc-scout__link:hover,.rc-scout__link:active{color:#4e8b1d;}</style>
|
|
</footer>
|
|
<script type="text/rocketscript">
|
|
(function(){
|
|
var twitterWidgets = document.createElement('script');
|
|
twitterWidgets.type = 'text/javascript';
|
|
twitterWidgets.async = true;
|
|
twitterWidgets.src = 'http://platform.twitter.com/widgets.js';
|
|
document.getElementsByTagName('head')[0].appendChild(twitterWidgets);
|
|
})();
|
|
</script>
|
|
</div>
|
|
</body>
|
|
</html>
|
|
|