Logo  

CS473 - Computer Networks

Basics of system calls:

In this course there are many system calls (calls directly to the kernel, though usually though a C library wrapper function,) and other user-space function calls which are normally listed by their name followed by a number enclosed in parenthesis, i.e. open(2). The number indicates which manual section that the given function name can be found, using the man command at a terminal prompt:

The man and info commands:

man [section] command|config_file|topic
man -k search
ex: > man man

Section What the section refers to
1 User Commands
2 System Calls
3 C Library Functions
4 Devices and Special Files
5 File Formats and Conventions
6 Games et. Al.
7 Miscellanea
8 System Administration tools and Deamons


info <command>

  • Like man, but for GNU utilities. Sometimes useful for additional information usually for gnu programs and utilities.

System calls

  • man 2 syscalls
    • (note: NOT man 2 syscall) for a list of system calls.

System calls are how programs interface with the Kernel, all input and output is ultimately accomplished via system calls. What follows are most of the non-networking system calls that would be useful for this course.

File I/O via system calls:

Files opened by a process are kept track of in the kernel by number (known as a descriptor), an unsigned integer starting at 0 and increasing up to the processes descriptor limit (Use the Unix command "limit descriptors" to view this descriptor limit)

A new process, created via the fork(2) system call, inherits all the open files of its parent. Files opened with the O_CLOEXEC flag are automatically closed upon a successful execve(2) system call.

The open(2) system call:

Files are opened with the open(2) system call.

    #include <sys/types.h>
    #include <sys/stat.h>
    #include <fcntl.h>

    int open(const char *pathname, int flags);
    int open(const char *pathname, int flags, mode_t mode);


The mode (file permissions bits) is only required when creating (O_CREAT) a file. The mode is modified by the users umask (set with the umask shell command.) realmode = (mode & ~umask)

flags are one of:

O_RDONLY read-only
O_WRONLY write-only
O_RDWR read/write

OR'ed (|) with zero or more of the following common flags:

O_CREAT Create the file if it does not exist.
O_TRUNC Truncate (zero out) the file upon opening
O_APPEND Append to the end of file.
... Many others

The return value is either a value of 0 or more (the file descriptor of the now opened file.) or a negative value indicating a failure to open the file.

Examples:

  • Opens /some/file.txt for reading:
    int fd = open("/some/file.txt", O_RDONLY);

  • Opens ./file.txt for writing, creating it if necessary and truncates it upon opening:
    int fd = open("file.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);

By default most processes with have three file descriptors given to them by the shell:

Descriptor C stream C define Usually
0 stdin STDIN_FILENO The keyboard
1 stdout STDOUT_FILENO Line-buffered terminal output
2 stderr STDERR_FILENO Unbuffered terminal output

Once a file is opened, data can be read/written to it with the read(2) and write(2) system calls.

The read(2) system call:

    #include <unistd.h>

    ssize_t read(int fd, void *buf, size_t count);


In POSIX, size_t represents the (unsigned) size of the largest in memory object that can be referenced (typically an unsigned long), ssize_t is the signed version of size_t, used to return negative values usually indicating failure.

buf is some memory buffer that must be at least count bytes in size.

The return value is the number of bytes read or -1 if an error occurred. A value of 0 typically means the end of file has been reached, but not in all cases. It is perfectly normal for read to read less than count number of bytes, so must always take that into consideration and use the return value of read.

The write(2) system call:

    #include <unistd.h>

    ssize_t write(int fd, const void *buf, size_t count);

The inverse of read. The return value indicates the number of bytes successfully written, which may be less than count in some cases (particularly for socket or non-blocking I/O.)

Example:

  char buf[BUFSIZE];
  int fd = open("file.txt", O_RDONLY);
  if (fd < 0) {
    perror("open");
    exit(1);
  }
  int r, w, t;  

  // Simple, no error checking:
  // Read up to BUFSIZE characters from the file (fd), until EOF is reached:
  while ((r = read(fd, buf, BUFSIZE)) > 0) {
    // Write what was read to the terminal (stdout):
    write(STDOUT_FILENO, buf, r);
  }

  // Makes sure that we write all that was read:
  // Still no error checking.
  while((r = read(fd, buf, BUFSIZE)) > 0) {
    t = 0;
    do {
      w = write(STDOUT_FILENO, buf+t, r-t);
      t += w;
    } while (t < r);
  }

Using errno:

When a system call is made, upon failure the global variable errno is set to the value of the error encountered. A user-readable error can be printed using perror(3) or strerror(3).

  #include <stdio.h>
  #include <errno.h>

  void perror(const char *s);
  #include <string.h>
  char *strerror(int errnum);

Examples:

  perror("Error message");
  fprintf(stderr,"Something happened [%s]\n", strerror(errno));

Processes and pipes

The fork(2) system call:

  #include <sys/types.h>
  #include <unistd.h>

  pid_t fork(void);


Creates a new process that is exactly identical to the parent with the exception that the return value of the fork() in the parent is either > 0 on success indicating the process ID of the child (or < 0 on error) or 0 if it is the child process.

Example:

  pid_t pid = fork();
  if (pid < 0) perror("fork");
  else if (pid > 0) {
    // parent code
  } else if (pid == 0) {
    // child code
  }

The pipe(2) system call:

  #include <unistd.h>

  int pipe(int pipefd[2]);


Modifies the pipefd array, placing two descriptors in in the pipefd elements on success (0 return value). The two descriptors in pipefd represent a uni-directional data pipe. Data written to pipefd[1] can be read out of pipefd[0].

                data
    pipefd[0] ◀────── pipefd[1]


While pipes may be used to pass data from one area of a program to another usually it is used to communicate between processes created with fork. The pipe is created prior to the fork and then used for communication after forking. If bi-directional communication is needed, either use two pipes or better use the bi-directional socketpair().

In order for EOF to be transmitted when the write end of the pipe is closed in the writer, the write end must also be closed in the reader.

  int pipefd[2];
  pipe(pipefd);

  pid_t pid = fork();
  if (pid > 0) {    // The parent will be the "writer":
    close(pipefd[0]);   // close the unnecessary read end:
    write(pipefd[1], "some text", 9);
    close(pipefd[1]);
  } else (pid == 0) {   // The child will be the "reader":
    close(pipefd[1]);
    r = read(pipefd[0], buf, K);
    write(1, buf, r);
    close(pipefd[0]);
  }
  • Increasing size of a pipe?
#include <unistd.h>
#include <fcntl.h>

int fcntl(int fd, int cmd, ...);

fcntl(fd, F_SETPIPE_SZ, 32768); // up to: /proc/sys/fs/pipe-max-size

The dup2(2) system call:

  #include <unistd.h>

  int dup2(int oldfd, int newfd);

clones oldfd as newfd, closing newfd if it is currently opened. It does this atomically which can be useful in multi-threaded applications. Returns the new file descriptor (>= 0) on success, -1 on failure.

Redirecting a file as a processes stdin:

   // Open the file we want to use as our stdin:
   int fd = open("some file", O_RDONLY);
   // Duplicate it on the stdin file descriptor (0):
   dup2(fd, STDIN_FILENO);
   // Close the un-needed original file descriptor.  The file is still opened
   // on stdin (descriptor 0):
   close(fd);


The above steps should be performed before exec'ing a program to make it read a file as it's stdin. Using dup()/dup2*( is not always necessary, one can sometimes just close the file descriptor to be replaced and then open the file to redirect the output to taking advantage of the open() semantics where it will always use the lowest available descriptor. Of course this will only work if there are no unused descriptor below the one we wish to replace.

  // Replace descriptor 2, i.e. stderr:
  close(2);
  // This open will re-use 2, assuming 0 and 1 are still in use.
  open(".error-log", O_WRONLY | O_APPEND | O_CREAT, 0666);

The wait(2) and waitpid(2) system calls:

  #include <sys/types.h>
  #include <sys/wait.h>

  pid_t wait(int *wstatus);
  pid_t waitpid(pid_t pid, int *wstatus, int options);

Any child process not waited on becomes a "zombie", a minimal process table entry that exists only to report the exit status of the process.

Each waits for a child process to finish. When this happens the parent is sent the SIGCHLD signal indicating the child has changed its run state, wait can then report the current status of the child stored in the integer pointed to as wstatus. Use the macro WEXITSTATUS(wstatus) to get the returned/exit value of the process as wstatus is an encoded value.

wait() usually blocks until the signal is received unless the WNOHANG option is used. Ideally you would use a signal handler for SIGCHLD in the parent to side-band call wait() in the handler.

The use of wait() can be avoided entirely if the SIGCHLD signal is explicitly ignored, in which case the init process (process ID #1) will "reap" the child process when it exits.

Example: signal(SIGCHLD, SIG_IGN);

Note that the newer sigaction(2) call should probably be used in lieu of signal(2).

The execve(2) system call:

  #include <unistd.h>

  int execve(const char *filename, char *const argv[], char *const envp[]);

The execve() and exec* C wrappers which call execve() replace the current process with the program specified by "filename", passing it the array of strings argv (terminated with a NULL) as it's command line parameters and envp as the array of name=value pairs representing it's environment (man 7 environ). execve() is not often used directly, but usually used via the more useful C wrappers, such as execlp(3).

Example:

  pid_t pid = fork();
  if (pid == 0) {
    // Runs the ls program, with the options -l and -a:
    char *args[] = {"ls", "-l", "-a", NULL};
    execve("/bin/ls", args, NULL);
  }
  // Wait for the ls program to complete before continuing
  int status;
  wait(&status);

Signals

Signals are like an empty message (though newer signal semantics allow one to transmit information, including file descriptors) to a process. Some signals will normally terminate the process (e.g. SIGKILL) or cause the process to suspend execution (e.g. SIGSTOP). The two signals SIGKILL and SIGSTOP cannot be caught, blocked or ignored by a process.

Signals can be sent to a process via the command line using the Unix commands:

> kill [ -# | -NAME ] process

  • terminate a process

> killall [-#|-NAME] processname

  • kill processes by name

> pkill

  • look up or signal processes based on name and other attributes

Sending signals in a program can be accomplished using the kill(2) system call:

    #include <sys/types.h>
    #include <signal.h>

    int kill(pid_t pid, int sig);

Signals can also be used to send a limited amount of information (an integer or a pointer or even a file descriptor) using the siqqueue(2) call:

    #include <signal.h>
    int sigqueue(pid_t pid, int sig, const union sigval value);

example

    union sigval sv;
    sv.sival_int = message;

    sigqueue(pid_t pid, int sig, sv);


sending signals to pids <= 0 have special actions, read man 2 kill for more information. Sending the signal 0 to a process does not send a signal to the process, but is useful to determine if the process exists.

Handling signals

Originally one used signal(2) to set the signal handler or to ignore signals, i.e.:

     #include <signal.h>

     // Defines the function pointer for a sighandler_t handler:
     typedef void (*sighandler_t)(int);

     sighandler_t signal(int signum, sighandler_t handler);


signal(2) installs a default signal handler, if handler is one of the following defines:

SIG_IGN Explicitly ignore the signal
SIG_DFL Use the default action for the signal (use this to remove a handler, for example.)

otherwise the handler should be a function pointer to the function that will handle the signal. When a signal arrives, the execution of the program is suspended and the handler function is called. When the handler returns, the normal execution of the program is resumed. If the program was attempting to make a system call, normally the call will be failed with a EINTR error placed in errno. Some system calls might be restarted, which is the default behavior for glibc 2 and above (BSD semantics), or as long as the SA_RESTART flag is used when using sigaction() to create the handler.

Signal examples:

   // Defines a signal handler:
   void handler(int sig) {
     // I/O with printf, etc is ill advised in a signal handler.
     ...
     // Restart signal handler, this is not necessary if using glibc 2 or later
     // as the C wrapper provides BSD semantics where the handler is persistent.
     signal(sig, &handler);
   }
   ...
   // Elsewhere in the program (such as in main):
   signal(SIGPIPE, &handler);


A Common loop to handle all signals:

for(i=0; i < 32; i++) signal(i, &handler);

sigaction(2)

signal(2) is depreciated in favor of sigaction(2) or signalfd.

    #include <signal.h>

    int sigaction(int signum, const struct sigaction *restrict act,
                  struct sigaction *restrict oldact);

    struct sigaction {
        void     (*sa_handler)(int);
        void     (*sa_sigaction)(int, siginfo_t *, void *);
        sigset_t   sa_mask;
        int        sa_flags;
        void     (*sa_restorer)(void);
    };

Examples

  void handler(int sig) {
    // I/O with printf, etc is ill advised in a signal handler.
    ...
    // sigaction handlers restart, so we don't need to reset them.
  }
  ...
  struct sigaction act;
  act.sa_handler = &sighandler; // Normal signal handler
  sigaction(SIGPIPE, &act, NULL);

To use the extended signal handler information:

  void sighandler(int sig, siginfo_t *info, void *ctxt) {
    printf("Caught: %d -> %d (pid: %d, uid: %d)\n", sig,
       info->si_value.sival_int, info->si_pid, info->si_uid);
    ...
  }

  struct sigaction act;
  act.sa_sigaction = &sighandler;
  act.sa_flags = SA_SIGINFO;
  sigaction(SIGALRM, &act, NULL);