pete > courses > CS 315 Fall 26 > lecture 02: C review, basic Linux usage, program design


Lecture 02: C review, basic Linux usage, program design

Goals


this course is going to be mostly about using system calls to build bigger and better software components that other programmers can use to themselves write applications for people to use

it’s also going to be about using various tools that make the task of programming easier, more efficient, and more effective

today is going to be a first look at basic usage of C, and of a few of the tools we’ll be using: namely, git, make, and manpages

both git and make are super complex; in this afternoon’s lab session, we will start with very simple usage and build up the complexity over the course of the semester

throughout the course, we will also discuss "conventions"—that is, accepted ways of solving particular problems or phrasing particular solutions (we’ll see some today)


speaking of tools, the most important one in your arsenal (for this class, and likely in general in your programming career) is a text editor

it’s exactly what it advertises to be: a program that edits text

my guess is that most of you have used Atom or VSCode for this purpose in the past

(I enumerate some other alternatives here—you are more than welcome to experiment in this course, but keep in mind some have a steep learning curve—vim is a particular offender in this regard, though I am more than happy to help you with it should you wish)

if you’re using a virtual machine, you will need to install your own text editor

you will find that you also need to install, eg, a compiler, as well as the other tools I’m going to talk about later today

instructions for installing software are available under the "Things you need to know" part of the Linux guide I wrote

if you’re using weathertop, it should have both text editors and compilers installed (though neither Atom nor VSCode)

you can actually use Atom or VSCode running your own computer to edit files located on another computer: instructions at the bottom of this page


I’d like to start with a few words about running programs on Linux

specifically using the command-line or shell interface (ie, the text-based window that may seem primitive but is actually more powerful than graphical interfaces in some important ways)

files are organized into hierarchies, just as on OS X and Windows, where folders can contain files or other folders, ad infinitum

the exception is that instead of the word "folders", we call them directories on Linux

the very top of this hierarchy is "/", which is pronounced "the root directory" (which is, confusingly, distinct from another directory, "/root", which is usually pronounced "slash root")


the shell is just a program that prints out a prompt and reads/executes what you type at it

(this is simplistic, but it will do for now; examples of other shell behavior will pop up throughout the course)

every running process, including this shell that I’m projecting on screen right now, has a notion of a current working directory ("cwd")

we can ask the shell to report its current working directory (also called the current directory):

$ pwd
/home/pete

we can also change the current working directory (this implies that a directory called "tmp" is one of the things in the directory called "/home/pete"):

$ cd tmp
$ pwd
/home/pete/tmp

we can refer to the parent directory by the special name ".."

$ cd ..
$ pwd
/home/pete

it might seem silly, but we can refer to the current directory by the special name "."

$ pwd
/home/pete
$ cd .
$ pwd
/home/pete

the names "tmp", "..", and "." refer to locations in the filesystem; the name for the thing that identifies a location in a filesystem is a path

therefore, we can say that the argument to cd is path to the directory we want to change to

you may observe that "/home/pete" and "/home/pete/tmp" also seem to be paths, but look a bit different; we’ll get to that in a sec


note that, in all three uses of cd above, the interpretation of the path given ("tmp" and "..") depends on the current directory

that is, "cd tmp" only worked because

if I was in a different directory (eg, "/home") that doesn’t itself contain a directory called "tmp", that command would not have worked:

$ cd ..
$ pwd
/home
$ cd tmp
bash: cd: tmp: No such file or directory

this is the shell (bash) telling us that it was unable to change directory because there is no such thing as "/home/tmp"


because the interpretation of "tmp", "..", and "." depends on the current working directory, these are called relative paths—they describe a location relative to where we are currently, and they mean nothing without knowing where we are currently

in contrast, paths like "/home", "/home/pete", and "/home/pete/tmp" are always meaningful: they start with "/", which is the very top of the directory hierarchy, and they always mean the same thing no matter what our current directory is

this latter type of path is called an absolute path

unless the documentation tells you otherwise, any program or function that uses paths will accept either absolute or relative paths

so this will work:

$ cd /home/pete/tmp
$ pwd
/home/pete/tmp

yes, this means that there are multiple ways to get where you want to go

as you get more comfortable using the command-line, you will get more efficient at specifying things like this


you might ask yourself how one might know what directories exist so that we might successfully change to them…

one uses the ls command to list files

when run with no parameters, ls lists all the files in the current directory:

$ ls

we can also ask ls to show us the files in another directory:

$ ls tmp

another way to modify the behavior of ls is to use command-line options

for instance, the -l option will show more information about each file:

$ ls -l
total 220
-rw-rw----  1 pete pete   3331 Sep  1 13:18  202-datapath-notes
drwxrwx---  2 pete pete   4096 Sep  9 12:18  bin
drwxrwx---  3 pete pete   4096 Apr 24 19:52  doc
drwxrwx---  2 pete pete   4096 Sep 15 22:54  log
drwxrwx---  2 pete pete   4096 Jun  2  2019  mnt
drwxrwx--- 33 pete pete   4096 Sep  1 00:22  scratch
drwxrwx--- 28 pete pete   4096 Oct 20  2020  shiznat
drwxrwx--- 33 pete pete   4096 Feb 16  2020  src
-rw-rw----  1 pete pete   6608 Sep 10 19:00  sway.out
drwxrwx---  8 pete pete   4096 Sep 15 22:54  tmp
-rw-rw----  1 pete pete 178332 Jun  4  2020 "what-did-ada-lovelace's-program-actually-do.pdf"

by columns: permissions, links, owner, group, size, modification time, name (will elaborate on these in a few minutes)

we can get a long listing of another directory by combining the -l option and the "non-option" argument demonstrated above:

$ ls -l tmp

in general, option arguments a) start with a '-' or '–' and b) modify how the program operates

and non-option arguments a) don’t start with a '-' or '–' and b) modify what the program operates on


you might be wondering: how does one know what arguments a particular program accepts and what do they do?

it is tradition in UNIX-like operating systems (of which Linux is one) to ship documentation with the OS—it is often referred to as the "manual" or the "online manual"

one can access the manual using the man command

$ man ls

(scroll using arrow keys, page-up, and page-down; exit by hitting 'q')

note that it describes the basic behavior and enumerates all the options it accepts

we’ll see manual pages in a slightly different context in a few minutes


back to the output of ls -l

one of the lines looked like this:

drwx------ 28 pete pete 4096 Sep 11 18:38 scratch

the first "pete" refers to the owner of the file and the second "pete" refers to the group that owns the file

so these are two different "pete"s: the first is the name of a user and the second is the name of a group

users you are probably familiar with through interacting with your own systems; groups maybe not so much

in UNIX (and therefore, in this case, in OS X), a user may be a member of multiple groups

you may see your group membership using the id program:

$ id
uid=1000(pete) gid=1000(pete) groups=1000(pete),998(wheel)

the "gid=1000(pete)" part says that my primary group is called "pete" and has id number 1000

the last part says that I am also a member of the "wheel" group (number 998)


the "drwx——" stuff describes the type and permissions of the file

the "d" means it’s a directory; "regular" files like programs and text files would have a "-" here; we’ll see a few other file types later, but these two are by far the most prevalent

as for the rest, UNIX defines three operations one might perform on a file: read, write, and execute

UNIX allows one to specify whether the user who owns the file can read, write, and/or execute it; separately, whether the group that owns the file can read, write, and/or execute it; and finally whether everyone else can read, write, or execute it (often referred to as "other")

the first triplet of those nine permissions characters refers to the permissions granted to the owner: in this case, the user pete can read, write, and execute the directory bin; but the user pete can only read and write the file notes.md

the second triplet applies to the group: any users in the group pete can read and execute the directory bin and only read the file notes.md

the final triplet applies to all users who are neither pete nor in the group pete—the "other"

(all running processes run on behalf of a user; we’ll see more of this later)


from the example above, the "drwx——" permissions means that the owner (pete) can read, write, and execute this file; members of the owning group (also named pete) can do nothing to it, and everyone else can do nothing either

as an alternative, check out this file:

$ ls -l /usr/bin/gcc
-rwxr-xr-x 3 root root 1132936 Jun 22 15:52 /usr/bin/gcc

this is a regular file (leftmost character is "-"), the owner can read/write/execute it, and everyone else can both read and execute it

(since gcc is installed in /usr/bin, it is assumed to be "blessed" by the system administrator and thus "more trustworthy", which is why only the root user can write it—if anybody could write to it, a bad person could put unwanted functionality into the compiler everybody is using)


on to C…

here’s the C program we’ll use as an example for the rest of this lecture: copy-file.c

to compile it:

$ gcc -Wall -pedantic -o copy-file copy-file.c

the -Wall and -pedantic parts ask gcc to give warnings if the code does anything sketchy (or, rather, it lowers the bar for what gcc consider sketchy)—and that I will require your submissions in this class to compile without warnings using these two flags

the -o copy-file part tells gcc what to name the resulting program (in this case, the output will be put in a file named "copy-file")

and the copy-file.c part tells gcc what source file(s) to read as input


you’re going to be writing a lot of software in this class, and invariably you will start with a prompt that describes what the software should do

in the case of today’s example, the program should copy the contents of one file into another file

taking what you know about manipulating files in Python and/or Java, what high-level steps would the program need to perform to carry out this task?

something like:

just as every programming task features a prompt of some kind, the first step is always to come up with a high-level outline of behavior just like this

I strongly suggest you do this in this course (and in your future programming endeavors) as it forces you think and focus your thoughts

additionally, I strongly suggest you use these as comments in your program: eg, copy-file-design.c


now that you’ve got the skeleton, pick a comment and ask yourself whether you can immediately implement the behavior it describes in code

if you can, great: do it

it you can’t, further break down the comment into smaller steps

rinse and repeat until you’ve got the program written

bonus: now it’s commented and you don’t need to make a mad scramble to add comments, hours after you wrote the corresponding code, just to appease the professor’s seemingly-arbitrary-but-not-really grading requirements

and doing so, you’d end up with something like the file linked before: copy-file.c

let’s walk through that now


first, the overall structure

just like public static void main in Java, the main function in C is the primary entry point for all programs: you must define this function and it is where execution begins

it accepts two parameters:

the count includes the name of the program itself, so if I run

$ ./copy-file input-file output-file

then argv[0] will be "./copy-file", argv[1] will be "input-file", and argv[2] will be "output-file"

like in Java, comments are bracketed in /* and */ pairs


now, the part that checks command-line arguments

/* check command-line arguments */
if(argc != 3) {
    printf("usage: %s source dest\n", argv[0]);
    exit(1);
}
src_filename = argv[1];
dst_filename = argv[2];

fairly simple: check that the correct number of arguments are provided

if not, print an error and exit

otherwise, save those arguments in well-named variables


printf is the primary function for performing output from C

in addition to printing, it also formats variables (hence the "f")

the first argument to printf ("usage: %s source dest\n") is the format string, which tells printf what the output should look like

any time the "%" symbol shows up in the format string, printf will replace it with the value of a parameter

in this case, the first one in the string ("%s") will be replaced by the value of the first parameter, argv[0]

the "s" in the format specifier indicates that the value should be interpreted as a string


you might wonder, given that the manual we looked at earlier documents programs, whether a similar mechanism exists for functions like printf

it does!

in fact, it’s the same command:

$ man printf

what you will see when you run this, however, is that the manpage that shows up does not appear to document a function called printf, but rather a program called printf

this is indeed the case

so what do we do when there exists both a program and a function with the same name?

it turns out the manual is broken into different sections: section 1 for programs and section 3 for functions (and section 2 for system calls, which we’ll see next week)

therefore, to see the documentation for the printf function, you need to tell man to look in section 3:

$ man 3 printf

much better


next part of the code:

/* open files */
in = fopen(src_filename, "r");
if(in == NULL) {
    perror("fopen");
    exit(2);
}

unsurprisingly, fopen opens a file

but what does the "r" mean? how can we know what parameters in general a function like fopen accepts? the manual!

$ man 3 fopen

and we see that "r" tells fopen to open the file for reading, whereas "w" opens the file for writing


also note that I’ve been pretty careful about making sure the functions worked: I check both their return values and exit if either fails

in other classes, my guess is you’ve not had to be particularly disciplined about checking return values

in this class, you will

recall that we are building software for other programmers to use: it is unreasonable to just randomly crash if something goes wrong: we need to provide more tools to our users to make sure our stuff gets used correctly

therefore, one of the requirements of your submissions will be that all return values are checked and the appropriate action taken in response to failure

in this case, either file failing to open means we can’t finish the job, and therefore exiting is the correct response


question, though: how do we know how fopen indicates an error?

read the RETURN VALUE part of the manpage (a standard part of all function manpages), which says:

Upon successful completion fopen(), fdopen() and  freopen()  return  a  FILE
pointer.   Otherwise,  NULL  is  returned  and  errno is set to indicate the
error.

easy peasy

but what is this errno thing?


errno is a global variable that indicates what kind of error has occured

therefore, when fopen encounters a problem, it will set this variable to a value that reflects what kind of problem it encountered, and then return NULL

when fopen does return, it is up to us to interpret that value

fortunately, there’s a function called perror that will print out a text representation of the error

we can test this:

$ ./copy-file foo bar
fopen: No such file or directory

unsurprising, because indeed the file foo does not exist


now to the other call to fopen, which does more or less the same thing but looks a little different:

if((out = fopen(dst_filename, "w")) == NULL) {
    perror("fopen");
    exit(3);
}

here I’ve used a C idiom (that is, a particular way of performing an operation that is widely used) to both perform an assignment and check its result on the same line of code

it is annoyingly complicated, at least until you get used to it, but you need to get used to it because it is widely used

it’s easiest to tackle from the inside out, so we start with the function call:

fopen(dst_filename, "w")

we can check the fopen manpage to see that this opens a file for writing

next we zoom out one level:

out = fopen(dst_filename, "w")

and see that the result of fopen is assigned to the variable out

zoom out once more:

if((out = fopen(dst_filename, "w")) == NULL)

and the result of the assignment is compared to NULL

which begs the question: what is the result of an assignment statement?

in C (but not in all languages!) the result of an assignment statement is the value assigned

so here we are checking whether the value stored in out is NULL

and therefore these four lines are philosophically equivalent to the five lines used to open the input file (though you will probably see the condensed version more often in the wild)


now, actually copying bits:

while((bytes_read = fread(buf, 1, BYTES_PER_ITERATION, in)) > 0) {
    bytes_written = fwrite(buf, 1, bytes_read, out);
    if(bytes_written == 0) {
        perror("fwrite");
        exit(4);
    }
}

the loop invariant is again an example of idiomatic C

again, the easiest way is to work from the inside out:

fread(buf, 1, BYTES_PER_ITERATION, in)

checking out the manpage for fread, we learn that this reads a certain number of bytes from the in and puts them into the chunk of memory pointed to by buf

looking to the top of the program, buf is declared thusly:

char buf[BYTES_PER_ITERATION];

recall that, in C, an array variable (in this case buf) is just a pointer to the first element of the array, and so buf is indeed a pointer and can be used as the first argument to fread

fread returns the number of bytes it actually read (which may be less than the number requested because the file may be smaller)


the result of this call is being saved in the variable bytes_read:

bytes_read = fread(buf, 1, BYTES_PER_ITERATION, in)

and then the result of this assignment is being compared to 0:

(bytes_read = fread(buf, 1, BYTES_PER_ITERATION, in)) > 0

again, in C, the result of an assignment is the value assigned

so this is an idiomatic way to both save the result of fread and compare it to zero

thus the loop will execute as long as fread returns a value greater than zero, which the manpage tells us it will do as long as it was able to read from the file (ie, as long as it doesn’t read the end of the file or encounter another error)


now, inside the loop, we take those bytes we just read from in and write them to out:

bytes_written = fwrite(buf, 1, bytes_read, out);

followed by our responsible error checking

note that we do NOT use BYTES_PER_ITERATION for the number of bytes to write: this is because fread may sometimes read fewer than this many (if the file isn’t big enough)

this is the kind of subtle behavior that is super important to get correct


after the loop:

/* the while loop above could terminate if fread encountered an error, so
 * check that and report if necessary */
if(ferror(in)) {
    perror("fread");
    exit(5);
}

recall that fread can return 0 under two cases: either it reached the end of the file (which is benign) or it encountered an error (which needs to be handled)

reading the fread manpage, we find that the ferror function tells us whether an error occurred, so we use that to be responsible here

(note also that this wasn’t in our original design: we had to modify our design based on what we learned about the behavior of fread(2). we also added a comment explaining why this check exists, because it’s not immediately apparent unless you really know the behavior of fread(3))


finally, we clean up in the obvious way:

/* clean up */
fclose(out);
fclose(in);

that should be enough to get you refreshed a bit with C

recall that you are welcome to consult external references when working on your assignments, with the only requirement being that you document which references you use

you are, of course, also most welcome to talk to me or the course assistants

Last modified: