Introduction

Hello, and welcome back to CS631 "Advanced Programming in the UNIX Environment". Last time, we revisited the struct stat with a focus on the atime, mtime, and ctime members. We noticed that things involving time can get a little confusing, but to be honest, that was nothing compared what we'll see in this video. Time is weird as a concept by itself, but our feeble attempts to make sense of our position in the universe and then to convince computers to play ball can lead to some entertaining surprises.

time(3)

But first, where does time come from? Well, as far as Unix is concerned, the kernel keeps track of time and provides it to any process asking for it based on counting quartz crystal oscillations or some such. That sounds complicated, so let's just gloss over that. Anyway, as we mentioned in our introductory session, Unix keeps track of time as seconds elapsed ѕince the epoch, midnight on January 1st, 1970. You can get this count back as an abstract data type time_t by calling the time(3) function. We've already talked about the problem of wrapping a 32-bit counter to represent this data type, leading to planes falling out of the sky and what not on January 19th, 2038, so we can gloss over that as well. Nice. Simple function, simple code example. Call time(3), print out the time_t, which conveniently converts into a long int and voila, we get a standard Unix timestamp of the current time as seconds since the epoch. But wait - time(3) is a library function, not a system call. So we know that time(3) itself doesn't actually get the time from? Easy enough to find out: use the source! Here we have the implementation of the time(3) function in libc, and it looks like all it does is call gettimeofday(2).

gettimeofday(2)

Here's what that syscall looks like. Like before, we get back what the system the current time is as seconds and microseconds elapsed since the epoch. The timeval struct has these two fields, and the second argument -- the struct timezone... ... is only provided for source compatibility and is ignored / set to 0 by gettimeofday(2). Sounds great, let's give it a try. Here, we add to our previous program a call to gettimeofday(2) and print out the results. As seen in the output here, the tv_sec field is the same as the time_t time(3) returned, but now we see microsecond granularity via the tv_usec field. But let's go back to the fact that the second field passed to gettimeofday(2) is being ignored. That seems odd, doesn't it? And also consider that we've seen that some Unix versions provide for example the various time fields in the struct stat with even finer granularity than microseconds, namely in nanoseconds. How do we get those?

clock_gettime(2)

Ok, so here we have the clock_gettime(2) system call. Which, not at all coincidentally, is what POSIX says one should use instead of gettimeofday(2). With this call, we pass in a struct timespec that the syscall fills in for us. This function then provides seconds _and nanoseconds_ since, yes, of course, the epoch. Let's add a call to clock_gettime(2) to our program. The output here looks good. We even see an increase in microseconds between the call to gettimeofday(2) and clock_gettime(2). So now we know how to get accurate time, stored in a struct timespec, a struct timeval, or a time_t, each giving us seconds since the epoch with increasing granularity. Alright, so seconds since the epoch. Typical Unix timestamp.

gmtime(3)

Awesome! But... I'm not a computer. I'm a human. And as a human, I'd much prefer a more readable date, since I don't like to count seconds since January 1st 1970. So... how do I turn a time_t into a human readable date? First, we have to break down the time_t into another data structure, a struct tm. For that, we use the gmtime(3) library function. The gmtime(3) function converts the time_t to Coordinated Universal Time, aka UTC. And the fact that "Coordinated Universal Time" is abbreviated as "UTC" really tells you all you need to know about how much sense computers make of time. The reason for this misnomer is of course that English speaking countries wanted to use "CUT" for Coordinated Universal Time, while French speaking countries wanted to use "Temps Universel Coordine", which would be TUC, so everybody agreed it should make no sense at all and be called "UTC" instead. "A successful compromise is when neither party is happy." Anyway, let's take a look at what this struct tm looks like. We get fields for seconds, minutes, hours, day of month, month, year etc. etc. But look at the comments. You'll quickly find a few quirky things here. For starters, we have the year being represented as number of years since 1900, not since e.g., the year 0. So while this is not _exactly_ a Y2K remnant, it can easily lead to a Y2K issue. Wait, do any of y'all even _remember_ Y2K? Well, look it up. Next, note that we count days from zero to 365, which adds up to 366 days. Alright, that's not so crazy: we have leap days, after all. So every four years, we get an extra day, and all of a sudden February has 29 days, and your calendar app you coded up fails in surprising ways. But leap years are not the only thing.

Leap Seconds

We also have something called a leap second. And this is where things get a bit complicated: We used to define seconds as derived from, effectively, how our planet moves around the little bit of space we could observe, notably that big yellow ball of fire in the sky. But then we changed the definition to be derived from the electron transition frequency of cesium atoms. But this time -- standardized as International Atomic Time or TAI, this time from the French name "temps atomique international" -- is not always in sync with how our planet bobs through space, so every so often we have to add or remove a second to make the two match. These discrepancies are observed by the International Earth Rotation and Reference System, which announces them to the public. So you can subscribe to the announcement mailing list, and then, every six months, you'll get one of the coolest emails ever: An email from the International Earth Rotation and Reference System to the "authorities responsible for the measurement and distribution of time", telling you whether or not 6 months later a second will be added or even removed. Here we see the email from the last time a leap second was added -- in 2016 -- and the mail from July noting that this December we will _not_ add a leap second. To date, we've had 27 leap seconds since they were introduced as a concept in 1972. But as fascinating as all that is, having leap seconds adds all sorts of headaches! For starters, POSIX says that seconds since the epoch monotonically increases and that implementations do not need to account for leap seconds. And so we get various problems when parsing or converting such dates. Here, we see the unix timestamp that translates to December 31st, 2016. 23:59:58 translates correctly, and so does 23:59:59. But 23:59:60 - the leap second - parses as midnight January 1st 2017. And the other way around doesn't work well, either: the leap second time parses as the same epoch timestamp as a second later. And then notice that our struct tm has values going up to _61_, meaning there are 62 valid values. But other Unix systems only allow up to 61 values. This is because there is a possibility of a _double_ leap second, even though we've not seen one so far. Or rather, there appears to have been some discussion about the possibility of a double leap second, and different Unix systems interpreted these decisions differently. And all of that doesn't even account for the possibility of a _negative_ leap second, whereby our cute idea of monotonically increasing time gets completely ruined. As I said, time is weird. But alright, we know how to break down a time_t into a struct tm, but we still haven't seen how we can format this struct tm into this convenient date string.

asctime(3)

For that, we have the asctime(3) function, which takes a struct tm and formats it into a string like this. So we can extend our program from before, and we then finally get the output of the timestamp formatted in a human readable date. But once we're in the business of representing time as strings suitable for human consumption, we're entering another world of confusion. Let's take another look at our struct tm -- there was one additional field: isdst, indicating whether or not Daylight Savings Time is in effect or not.

Timezones

In fact, there may be further flags not specified by POSIX in the struct tm that also concern themselves with this concept of Daylight Saving time as well as the local timezone, since, as it turns out, not all people around the world are located in Greenwich, England or anywhere else along the zero meridian. So we probably want to be able to display time in the local timezone. For that, we have the localtime(3) function. Just like gmtime(3), it breaks a time_t into a struct tm, but unlike gmtime(3), localtime(3) accounts for the local timezone offset and possible DST. Let's add this call to our program. Hmm, there doesn't seem to be much of a difference here. Why is that? Well, our system is not configured to use a specific timezone, and so, like any reasonable production system, operates in UTC by default. But fortunately localtime(3) is smart enough to honor the TZ environment variable, and so we can set a timezone to use by specifying, for example, US/Eastern, in which case we do get the current four hour time difference to UTC. Timezones, by the way, are completely nuts and an endless headache for software engineers everywhere. Here, have a look at a map displaying the current timezones across the world. Take some time lateron to zoom in and look at how the boundaries run. Often times they are nearly or completely arbitrary of the actual observed solar time and instead drawn based on political borders. And while most are reasonably easy to juggle one-hour increments off the zero meridian, every now and then you encounter half-hour increments. But where things go really wild is in Antarctica: there, timezones are assigned more or less based on which country owns and operates the research station in a given region. So the Troll research station is operated by Norway, and so it operates in UTC+2, except during summer, when it is on Daylight Savings Time and runs in UTC. Only that's when it's actually _winter_ in the southern hemisphere and trying to "save" any daylight in Antarctica really is not a concept that makes a whole lot of sense. Of course this is done to simply keep communications easier with researches in Norway on the other side of the globe, but it gives you an idea how silly time can become.

Daylight Savings Time

Daylight Savings Time itself is all sorts of ridiculous, too. First of all, most the countries in the world don't have this concept. Then there are parts in some countries that _do_ use daylight savings time that don't. And then there's the fact that this changes _all the time_. Different countries make decisions around when to start or end Daylight Savings Time with surprising frequency. For example, the US changed the dates on when we switch on and off DST back in 2007; Europe just voted to get rid off DST altogether starting next year, and so on and so on. So if you want to know how to format time in a manner that honors the timezone and the local Daylight Savings Time rules, you need to know what those rules are _and keep them up to date_. This is a many-to-many mapping... as shown here. This is an illustration of all the timezone data you need to have to manage time for us silly humans who don't want to simply count seconds since January 1st, 1970. Your Unix system has this information stored under /usr/share/zoneinfo. This data is provided to you with the operating system, but obviously we all all need to agree on that data across operating systems. And so this data is managed by IANA, the Internet Assigned Numbers Authority, and yes, you actually do need to keep this data up to date on a regular basis. If you want to check what time it is in a given location, you can run through all available zones like this.

mktime(3)

Ok, next, let's take a look at how we can manage time that's not the current time. For that, we can create our own struct tm, set the various fields as we like, and then call mktime(3), and we get back a time_t. That is, mktime(3) operates in the inverse direction from gmtime/localtime. and if we have a time_t instead of a struct tm, we can use ctimez(3) instead of asctime(3) to print it. Here, let's add code to print the date at the epoch. Easy enough. But humans are silly - did I mention that? Sometimes they have different ideas about how they would like dates to be represented and it's not the format ctime(3) spits out. Here, the "date" command gives output in this format... ...but we can ask it to use a different format. This format, by the way, is terrible, because in some countries the first field represents the day and the second one the day, while in others it's the other way around. Which is why the only sane human-readable timezone format is this -- ISO8601, where date and time are monotonically increasing and the timezone information is added at the end, but since we're not ridiculous, we don't even use a timezone and instead use Zulu time, wnich, seriously, stands for UTC. "Zulu time" just sounds a lot cooler. Anyway, so if we want to format a struct tm based on a format specifier as shown here in the date(1) command, then we can use strftime(3).

strftime(3)

In practice, that looks like so: We avoid magic numbers and define our buffer length as a meaningfully named constant... ...fill that buffer via strftime... et voila: get our epoch struct tm in human readable format.

Recap

Alright, I think we've had just about enough with converting time between the different formats. Here's this entire lecture in a single graphic. Yes, I could have just showed you this picture at the beginning and said "well, that's about it", but that wouldn't have been quite as much fun, now would it? So: the kernel provides you a time_t, which you can break down into a struct tm via gmtime(3) or localtime(3); we can convert a struct tm into a time_t via mktime(3), and we can convert these into human friendly strings via ctime(3), asctime(3) or strftime(3). Other than that, we note that time is largely an illusion, computers struggle with representing it, but most problems derive from humans having different ideas about what it is or should be. Which means that programming anything that handles time in any way, shape, or form is likely to hide a number of surprising bugs. Check out the link to the "Falsehoods Programmers Believe About Time" in the lecture slides. It's both entertaining and quite impressive. This discussion also serves as a reminder that all programming touches or is influenced by geopolitical events in often unobvious ways. And with that, let's call it time for today. Thanks for watching - cheers!