Duke Systems CPS 310 Unix Broadly Defined Jeff
Duke Systems CPS 310 Unix Broadly Defined Jeff Chase Duke University http: //www. cs. duke. edu/~chase/cps 310
The story so far: process and kernel • A (classical) OS lets us run programs as processes. A process is a running program instance (with a thread). – Program code runs with the core in untrusted user mode. • Processes are protected/isolated. – Virtual address space is a “fenced pasture” – Sandbox: can’t get out. Lockbox: nobody else can get in. • The OS kernel controls everything. – Kernel code runs with the core in trusted kernel mode. • OS offers system call APIs. – Create processes – Control processes – Monitor process execution
Processes and the kernel Programs run as independent processes. data Protected system calls Protected OS kernel mediates access to shared resources. Each process has a private virtual address space and one thread. . and upcalls (e. g. , signals) Threads enter the kernel for OS services. The kernel is a separate component/context with enforced modularity. The kernel syscall interface supports processes, files, pipes, and signals.
Unix fork/exec/exit/wait syscalls fork child fork parent program initializes child context exec int pid = fork(); Create a new process that is a clone of its parent. exec*(“program” [argvp, envp]); Overlay the calling process with a new program, and transfer control to it, passing arguments and environment. exit(status); Exit with status, destroying the process. wait exit int pid = wait*(&status); Wait for exit (or other status change) of a child, and “reap” its exit status. Recommended: use waitpid().
Unix fork/exit syscalls int pid = fork(); Create a new process that is a clone of its parent, return new process ID (pid) to parent, return 0 to child. fork parent child exit(status); Exit with status, destroying the process. Note: this is not the only way for a process to exit! time data exit data p exit pid: 5587 pid: 5588
fork The fork syscall returns twice: int pid; int status = 0; if (pid = fork()) { /* parent */ …. . } else { /* child */ …. . exit(status); } It returns a zero in the context of the new child process. It returns the new child process ID (pid) in the context of the parent.
exit syscall
fork (original concept)
fork in action today void dofork() { int rc = fork(); if (rc < 0) { perror("fork failed: "); exit(1); } else if (rc == 0) { child(); } else { parent(rc); } } Fork is conceptually difficult but syntactically clean and simple. I don’t have to say anything about what the new child process “looks like”: it is an exact clone of the parent! The child has a new thread executing at the same point in the same program. The child is a new instance of the running program: it has a “copy” of the entire address space. The “only” change is the process ID and return code rc! The parent thread continues on its way. The child thread continues on its way.
A simple program: forkdeep int count = 0; int level = 0; void child() { level++; output pids if (level < count) dofork(); if (level == count) sleep(3); } void parent(int childpid) { output pids wait for child to finish How? } main(int argc, char *argv[]) { count = atoi(argv[1]); dofork(); output pid }
chase$. /forkdeep 4 30866 -> 30867 -> 30868 -> 30869 -> 30870 30869 30868 30867 30866 chase$
wait Note: in modern systems the wait syscall has many variants and options.
wait int pid; int status = 0; if (pid = fork()) { /* parent */ …. . pid = wait(&status); } else { /* child */ …. . exit(status); } Parent uses wait to sleep until the child exits; wait returns child pid and status. Wait variants allow wait on a specific child, or notification of stops and other “signals”.
Thread states and transitions We will presume that these transitions occur only in kernel mode. This is true in classical Unix and in systems with pure kernel-based threads. Before a thread can sleep, it must first enter the kernel via trap (syscall) or fault. Before a thread can yield, it must enter the kernel, or the core must take an interrupt to return control to the kernel. running sleep blocked STOP wait wakeup yield preempt dispatch ready On entry to the running state, kernel code decides if/when/how to enter user mode, and sets up a suitable context.
Kernel Stacks and Trap/Fault Handling Threads execute user code on a user stack in the user virtual memory in the process virtual address space. Each thread has a second kernel stack in kernel space (VM accessible only in kernel mode). data stack syscall dispatch table stack System calls and faults run in kernel mode on a kernel stack. Kernel code running in P’s process context has access to P’s virtual memory. The syscall handler makes an indirect call through the system call dispatch table to the handler registered for the specific system call.
Running a program sections code (“text”) constants initialized data Process segments data Program Unix: fork/exec Thread virtual memory When a program launches, the OS creates a process to run it, with a main thread to execute the code, and a virtual memory to store the running program’s code and data.
But how do I run a new program? • The child, or any process really, can replace its program in midstream. • exec* system call: “forget everything in my address space and reinitialize my entire address space with stuff from a named program file. ” • The exec system call never returns: the new program executes in the calling process until it dies (exits). – The code from the parent program runs in the child process and controls its future. The parent program selects the program that the child process will run (via exec), and sets up its connections to the outside world. The child program doesn’t even know!
exec (original concept)
A simple program: forkexec … main(int argc, char *argv[]) { int status; int rc = fork(); if (rc < 0) { perror("fork failed: "); exit(1); } else if (rc == 0) { argv++; execve(argv[0], argv, 0); } else { waitpid(rc, &status, 0); printf("child %d exited with status %dn", rc, WEXITSTATUS(status)); } }
A simple program: prog 0 int main() { } chase$ cc –o forkexec. c chase$ cc –o prog 0. c chase$. /forkexec prog 0 child 19175 exited with status 0 chase$
Unix fork/exec/exit/wait syscalls fork parent program initializes child process context fork child exec int pid = fork(); Create a new process that is a clone of its parent. exec*(“program” [argvp, envp]); Overlay the calling process with a new program, and transfer control to it, passing arguments and environment. exit(status); Exit with status, destroying the process. wait exit int pid = wait*(&status); Wait for exit (or other status change) of a child, and “reap” its exit status. Recommended: use waitpid().
Mode Changes for Fork/Exit • Syscall traps and “returns” are not always paired. – fork “returns” (to child) from a trap that “never happened” – exit system call trap never returns – System may switch processes between trap and return parent Fork call Fork return Wait call Wait return Exec enters the child by doctoring up a saved user context to “return” through. child Fork entry to user space Exit call transition from user to kernel mode (callsys) transition from kernel to user mode (retsys)
But how is the first process made?
Init and Descendents Kernel “handcrafts” initial process to run “init” program. Other processes descend from init, including one instance of the login program for each terminal. Login runs user shell in a child process after user authenticates. User shell runs user commands as child processes.
Processes: A Closer Look virtual address space + The address space is a private name space for a set of memory segments used by the process. The kernel must initialize the process memory for the program to run. thread stack process descriptor (PCB) + Each process has a thread bound to the VAS. The thread has a stack addressable through the VAS. The kernel can suspend/restart the thread wherever and whenever it wants. user ID process ID parent PID sibling links children resources The OS maintains some state for each process in the kernel’s internal data structures: a file descriptor table, links to maintain the process tree, and a place to store the exit status.
The Shell • Users may select from a range of command interpreter (“shell”) programs available. (Or even write their own!) – csh, ksh, tcsh, bash: choose your flavor… • Shells execute commands composed of program filenames, args, and I/O redirection symbols. – Fork, exec, wait, etc. – Can coordinate multiple child processes that run together as a process group or job. – Shells can run files of commands (scripts) for more complex tasks, e. g. , by redirecting I/O channels (descriptors). – Shell behavior is guided by environment variables, e. g. , $PATH – Parent may control/monitor all aspects of child execution.
Unix process: parents rule Created with fork by parent program running in parent process. Parent program running in child process, or exec’d program chosen by parent program. Inherited from parent process, or modified by parent program in child Virtual address space (Virtual Memory, VM) Process text data heap Thread Program kernel state Clone of parent VM. Environment (argv[] and envp[]) is configured by parent program on exec.
Exec setup (ABI)
Unix: upcalls • The kernel directs control flow into user process at a fixed entry point: e. g. , entry for exec() is _crt 0 or “main”. • Process may also register a signal handlers for events relating to the process, (generally) signalled by the kernel. • To deliver a signal, kernel munges/redirects the thread context to execute the signal handler in user mode. • Process lives until it exits voluntarily or fails – “receives an unhandled signal that is fatal by default”. data Protected system calls data . . . and upcalls (e. g. , signals)
Unix: signals • A signal is a typed upcall event delivered to a process by kernel. – Process P may use kill* system call to request signal delivery to Q. – Process may register signal handlers for signal types. A signal handler is a procedure that is invoked when a signal of a given type is delivered. Runs in user mode, then returns to normal control flow. – A signal delivered to a registered handler is said to be caught. If there is no registered handler then the signal triggers a default action (e. g. , “die”). • A process lives until it exits voluntarily or receives a fatal signal. data Protected system calls data . . . and upcalls (signals) Kernel sends signals, e. g. , to notify processes of faults.
Unix process view: data I/O channels (“file descriptors”) stdin stdout tty VM mappings Process stderr text data pipe Thread socket anon VM etc. Files Program Labels: uid etc. Segments
Unix I/O and IPC • I/O objects / kernel abstractions / channel types: – Terminal: user interaction via “teletypewriter”: tty – Pipe: Inter Process Communication (IPC) – Socket: networking – File: storage • Signals for IPC, process control, faults • Some shellish grunge needed for Lab 2 “typewriter”
Unix process view: data A process has multiple channels for data movement in and out of the process (I/O). I/O channels (“file descriptors”) stdin stdout tty The parent process and parent program set up and control the channels for a child (until exec). Process stderr pipe Thread socket Files Program
Standard I/O descriptors I/O channels (“file descriptors”) tty stdin stdout Open files or other I/O channels are named within the process by an integer file descriptor value. stderr Standard descriptors for primary input (stdin=0), primary output (stdout=1), error/status (stderr=2). These are inherited from the parent process and/or set by the parent program. By default they are bound to the controlling terminal. count = read(0, buf, count); if (count == -1) { perror(“read failed”); /* writes to stderr */ exit(1); } count = write(1, buf, count); if (count == -1) { perror(“write failed”); /* writes to stderr */ exit(1);
Files A file is a named, variable-length sequence of data bytes that is persistent: it exists across system restarts, and lives until it is removed. fd = open(name, <options>); write(fd, “abcdefg”, 7); read(fd, buf, 7); lseek(fd, offset, SEEK_SET); close(fd); creat(name, mode); mkdir(name, mode); rmdir(name); unlink(name); An offset is a byte index in a file. By default, a process reads and writes files sequentially. Or it can seek to a particular offset.
Files: hierarchical name space root directory applications etc. mount point user home directory external media volume or network storage
Unix “file descriptors” illustrated user space kernel space file int fd pointer per-process descriptor table pipe socket Disclaimer: this drawing is oversimplified tty system-wide open file table Processes often reference OS kernel objects with integers that index into a table of pointers in the kernel. (Why? ) Windows calls them handles. In Unix, processes may share I/O objects (i. e. , “files”: in Unix “everything is a file”). But the descriptor name space is per-process: fork clones parent descriptor table for child, but then they may diverge.
Processes reference objects text Files data anon VM
Fork clones all references text Files data anon VM Cloned file descriptors share a read/write offset. The kernel objects referenced by a process have reference counts. They may be destroyed after the last ref is released, but not before. Cloned references to VM segments are likely to be copy-on-write to create a lazy, virtual copy of the shared object. What operations release refs?
Shell and child 1 tty 3 stdin dsh fork tcsetpgrp exec wait stdout stderr tty stdin dsh stdout stderr 2 tty stdin stdout stderr Child process inherits standard I/O channels to the terminal (tty). If child is to run in the foreground: Child receives/takes control of the terminal (tty) input (tcsetpgrp). The foreground process receives all tty input until it stops or exits. The parent waits for a foreground child to stop or exit.
Pipes pipe A pipe is a bounded kernel buffer for passing bytes. int pfd[2] = {0, 0}; pipe(pfd); /*pfd[0] is read, pfd[1] is write */ b = write(pfd[1], "12345n", 6); b = read(pfd[0], buf, b); b = write(1, buf, b); 12345 • The pipe() system call creates a pipe object. • A pipe has one read end and one write end: unidirectional. • Bytes placed in the pipe with write are returned by read in order. • The read syscall blocks if the pipe is empty. • The write syscall blocks if the pipe is full. • Write fails (SIGPIPE) if no process has the other end open.
A key idea: Unix pipes [http: //www. bell-labs. com/history/unix/philosophy. html]
Unix programming environment Standard unix programs read a byte stream from standard input (fd==0). stdin They write their output to standard output (fd==1). stdout Stdin or stdout might be bound to a file, pipe, device, or network socket. If the parent sets it up, the program doesn’t even have to know. That style makes it easy to combine simple programs using pipes or files. The processes may run concurrently and are automatically synchronized
Shell pipeline example tty stdin chase$ who | grep chase console Jan 13 21: 08 chase ttys 000 Jan 16 11: 37 chase ttys 001 Jan 16 15: 00 chase$ dsh stdout stderr tty Job stderr stdout stdin stderr stdin pipe who stdout grep tty
But how to rewire the pipe? 1 P creates pipe. P 2 P forks C 1 and C 2. Both children inherit both ends of the pipe, and stdin/stdout/stderr. Parent closes both ends of pipe after fork. tty stdout stdin stdout C 1 3 A stdin C 1 closes the read end of the pipe, closes its stdout, “dups” the write end onto stdout, and execs. tty C 2 3 B C 2 closes the write end of the pipe, closes its stdin, “dups” the read end onto stdin, and execs.
Unix dup* syscall int fd 2 = 0 tty in int fd 1 = 5 tty out per-process descriptor table pipe out pipe in Hypothetical initial state before dup 2(fd 1, fd 2) syscall. dup 2(fd 1, fd 2). What does it mean? Yes, fd 1 and fd 2 are integer variables. But let’s use “fd” as shorthand for “the file descriptor whose number is the value of the variable fd”. Then fd 1 and fd 2 denote entries in the file descriptor table of the calling process. The dup 2 syscall is asking the kernel to operate on those entries in a kernel data structure. It doesn’t affect the values in the variables fd 1 and fd 2 at all!
Unix dup* syscall int fd 2 = 0 int fd 1 = 5 X > per-process descriptor table tty in tty out pipe in Final state after dup 2(fd 1, fd 2) syscall. Then dup 2(fd 1, fd 2) means: “close(fd 2), then set fd 2 to refer to the same underlying I/O object as fd 1. ” It results in two file descriptors referencing the same underlying I/O object. You can use either of the descriptors to read/write. But you should probably just close(fd 1).
Unix dup* syscall int fd 2 = 0 int fd 1 = 5 tty in X per-process descriptor table tty out pipe in Final state after dup 2(fd 1, fd 2) syscall. Then dup 2(fd 1, fd 2); close(fd 1) means: “remap the object referenced by file descriptor fd 1 to fd 2 instead”. It is convenient for remapping descriptors onto stdin, stdout, stderr, so that some program will use them “by default” after exec*. Note that we still have not changed the values in fd 1 or fd 2. Also, changing the values in fd 1 and fd 2 can never affect the state of the entries in the file descriptor table. Only the kernel can do that.
Simpler example Feeding a child through a pipe Parent ifd[2] = {0, 0}; stdin pipe(ifd); cid = fork(); Child close(0); close(ifd[1]); dup 2(ifd[0], 0); close(ifd[0]); execve(…); parent stdout stderr pipe stdin stdout stderr child cid Parent close(ifd[0]); count = read(0, buf, 5); count = write(ifd[1], buf, 5); waitpid(cid, &status, 0); printf("child %d exited…”); chase$ man dup 2 chase$ cc -o childin. c chase$. /childin cat 5 12345 5 bytes moved child 23185 exited with status 0 chase$
Notes on pipes • All the processes in a pipeline run concurrently. The order in which they run is not defined. They may execute at the same time on multiple cores. • Their execution is “synchronized by producer/consumer bounded buffer”. (More about this next month. ) It means that a writer blocks if there is no space to write, and a reader blocks if there are no bytes to read. Otherwise they may both run: they might not, but they could. • The pipe itself must be created by a common parent. The children inherit the pipe’s file descriptors from the parent on fork. Unix has no other way to pass a file descriptor to another process. • How does a reader “know” that it has read all the data coming to it through a pipe? Answer: there are no bytes in the pipe, and no process has the write side open. Then the read syscall returns EOF. By convention a process exits if it reads EOF from stdin.
Pipe reader and writer run concurrently concept reality context switch Context switch occurs when one process is forced to sleep because the pipe is full or empty, and maybe at other times.
Platform abstractions • Platforms provide “building blocks”… • …and APIs to use them. – Instantiate/create/allocate – Manipulate/configure – Attach/detach – Combine in uniform ways – Release/destroy The choice of abstractions reflects a philosophy of how to build and organize software systems.
Map. Reduce/ Hadoop Dataflow programming Map. Reduce is a filter-and-pipe model for data-intensive cluster computing. Its programming model is “like” Unix pipes. It adds “parallel pipes” (my term) that split output among multiple downstream children.
Sockets socket A socket is a buffered channel for passing data over a network. client int sd = socket(<internet stream>); gethostbyname(“www. cs. duke. edu”); <make a sockaddr_in struct> <install host IP address and port> connect(sd, <sockaddr_in>); write(sd, “abcdefg”, 7); read(sd, …. ); • The socket() system call creates a socket object. • Other socket syscalls establish a connection (e. g. , connect). • A file descriptor for a connected socket is bidirectional. • Bytes placed in the socket with write are returned by read in order. • The read syscall blocks if the socket is empty. • The write syscall blocks if the socket is full. • Both read and write fail if there is no valid connection.
A simple, familiar example request “GET /images/fish. gif HTTP/1. 1” reply client (initiator) server sd = socket(…); connect(sd, name); write(sd, request…); read(sd, reply…); close(sd); s = socket(…); bind(s, name); sd = accept(s); read(sd, request…); write(sd, reply…); close(sd);
Unix: simple abstractions? • • users files processes pipes – which “look like” files These persist across reboots. They have symbolic names (you choose it) and internal IDs (the system chooses). These exist within a running system, and they are transient: they disappear on a crash or reboot. They have internal IDs. Unix supports dynamic create/destroy of these objects. It manages the various name spaces. It has system calls to access these objects. It checks permissions.
Unix: a lesson here • Classical Unix programs are packaged software components: specific functions with narrow, text-oriented interfaces. • Shell is a powerful (but character-based) user interface. • It is also a programming environment for composing and coordinating programs interactively. • So: it’s both an interactive programming environment and a programmable user interface. – Both ideas were “new” at the time (late 1960 s). – Unix has inspired a devoted (druidic? ) following over 40+ years. • Its powerful scripting environment is widely used in large-scale system administration and services. • And it’s inside your laptop, smartphone, server, cloud.
Unix defines uniform, modular ways to combine programs to build up more complex functionality. Other application programs sh nroff cpp who a. out Kernel comp date Hardware cc wc as ld vi ed grep Other application programs
Unix: A lasting achievement? “Perhaps the most important achievement of Unix is to demonstrate that a powerful operating system for interactive use need not be expensive…it can run on hardware costing as little as $40, 000. ” DEC PDP-11/24 The UNIX Time-Sharing System* D. M. Ritchie and K. Thompson 1974 http: //histoire. info. online. fr/pdp 11. html
Let’s pause a moment to reflect. . . From Hennessy and Patterson, Computer Architecture: A Quantitative Approach, 4 th edition, 2006 Core Rate (SPECint) Note log scale Today Unix runs embedded in devices costing < $100.
Small is beautiful? The UNIX Time-Sharing System* D. M. Ritchie and K. Thompson 1974
Unix, looking backward: UI+IPC • Conceived around keystrokes and byte streams – User-visible environment is centered on a text-based command shell. • Limited view of how programs interact – files: byte streams in a shared name space – pipes: byte streams between pairs of sibling processes
X Windows (1985) Big change: GUI. 1. Windows 2. Window server 3. App events 4. Widget toolkit
Unix, looking backward: security • Presumes multiple users sharing a machine. • Each user has a user. ID. – User. ID owns all files created by all programs user runs. – Any program can access any file owned by user. ID. • Each user trusts all programs it chooses to run. – We “deputize” every program. – Some deputies get confused. – Result: decades of confused deputy security problems. • Contrary view: give programs the privileges they need, and nothing more. – Principle of Least Privilege
These slides are relevant to the shell lab, but they weren’t discussed in class and the material won’t be tested (unless it also appeared somewhere else).
Jobs and job control • A job is a set of processes that run together. – The processes are joined in a process group. – The process group leader is the first process in the set, e. g. , the leftmost process in a pipeline (other jobs have only one process). • A foreground job receives/takes control of the tty. – The parent passes control; the job’s process group takes control; parent waits on processes in group. Call this “set-fg”. • When a foreground job stops or completes, the parent awakes and receives/takes back control of the tty. • These mechanisms exist to share the controlling tty among multiple processes/jobs that are active at the same time. • Before the advent of GUIs, each user had only one tty! Job control was an important innovation that enabled users to “multitask”.
Process goups • The process(es) of a job run as a process group. – Process group ID (pgid) is pid of leader. – Every process is in exactly one process group. – Exactly one process group controls the tty. – The members of the controlling process group are foreground. • If a user types ctrl-c (cancel) or ctrl-z (snooze): – The kernel (tty driver) sends a signal to all members of the controlling process group. They die or stop if they don’t catch it. – The shell keeps its own process group separate from its children, so it doesn’t receive their signals. • If a process does a read from the tty: – If it is not in the foreground then it receives a signal (SIGTTIN). It stops if it doesn’t catch it. Optionally, write generates a signal too (SIGTTOU).
Job states and transitions User can send a STOP signal to a foreground process/job by typing ctrl-z on tty. Continue a stopped process or job by sending it a SIGCONT signal with “kill” or “killpg” syscall. ctrl-c: (SIGINT) fork + set-fg SIGCONT set-fg fg ctrl-z (SIGTSTP) exit EXIT exit SIGCONT STOP stop tty in tty out bg P receives SIGTTIN if it attempts to read from tty and is not in controlling process group. Optionally, P receives SIGTTOU if it attempts to write to tty and is not in controlling process group. Default action of these signals is STOP.
Unix signals • Unix signals are “musty”. We don’t teach/test the details. – Sometimes you can’t ignore them! • A signal is an “upcall” event delivered to a process. – Sent by kernel, or by another process via kill* syscall. – An arriving signal transfers control to a handler routine in receiving process, designated to handle that type of signal. – The handler routine returns to normal control flow when done. • Or: if no designated handler, take a standard action. – e. g. , DEFAULT, IGNORE, STOP, CONTINUE – Default action is often for receiving process to exit. – An exit from an unhandled signal is reported to parent via wait*.
Signals and dsh • Your shell must set up itself and its children properly for “stuff to work like it should”. – We have provided you code to initialize shell, parse, etc. – You do not need to understand that code – You should not need to mess with that code. – Please don’t mess with that code. – If you are messing with that code, please talk to us. – What that code does: transfer control of terminal to foreground child job, using process groups and signals, prepare for proper handling of various events that might occur.
You shouldn’t have to know How to seize control of the terminal if (tcsetpgrp(0, getpid()) < 0) { perror("tcsetpgrp failure"); exit(EXIT_FAILURE); } Translation: “Terminal control set process group for stdin (file descriptor 0) to the process group of which I am a leader. Stdin is a tty and I want control of it. ” You can also use tcsetpgrp to pass control of the terminal to some other process group named by pgid.
Shell and children dsh Any of these child jobs/processes could exit or stop at any time. & P 1 A P 1 B P 3 A Job 1 & P 2 A Job 2 Job 3 How should a process wait for the next event, if there are many possible events to wait for? If the process blocks to wait for one event, could it miss another event that happens first?
Monitoring background jobs dsh Parent can use a non-blocking wait syscall to poll (query) the status of a child. “echo y&” fork “jobs” exec waitpid(pid, &status, WNOHANG); wait EXIT But how should a parent learn of status changes to a child if parent is busy with something else and does not poll? “Do you know where your children are? ”
Monitoring background jobs dsh What if a child changes state while the shell is blocked on some other event, e. g. , waiting for input, or waiting for a foreground job to complete? “echo y&” coffee…. fork exec EXIT A “real” shell should notice and inform the user immediately. But dsh will not. Parent is waiting for read to return the next input command. How should parent learn of the child exit event?
Monitoring background jobs dsh A “real” shell receives SIGCHLD signals from the kernel when a child changes state. “echo y&” coffee…. If the shell is blocked on some other system call (e. g. , read), then SIGCHLD interrupts the read. Key point: many programs need to be able to receive and handle multiple event types promptly, regardless of what order they arrive in. Unix has grown some funky mechanisms to deal with that. fork exec Event notifications STOP SIGCHLD EXIT
Unix I/O: the basics char buf[BUFSIZE]; size_t count = 5; chase$ cc –o cat 5. c chase$. /cat 5 <waits for tty input> tty input: 12345<enter> output: 12345 5 bytes moved chase$ count = read(0, buf, count); if (count == -1) { perror(“read failed”); exit(1); } count = write(1, buf, count); if (count == -1) { perror(“write failed”); exit(1); } fprintf(stderr, “n%zd bytes movedn”, count); TTY read blocks until <enter> or <EOF>, then returns all bytes entered on the line. A read returns 0 after <EOF>.
Unix I/O: the basics char buf[BUFSIZE]; size_t count = 5; count = read(0, buf, count); if (count == -1) { perror(“read failed”); exit(1); } count = write(1, buf, count); if (count == -1) { perror(“write failed”); exit(1); } fprintf(stderr, “n%zd bytes movedn”, count); chase$. /cat 5 1234 5 bytes moved chase$. /cat 5 123 4 bytes moved chase$. /cat 5 123456 12345 5 bytes moved chase$ 6 -bash: 6: command not found chase$
Unix I/O: the basics char buf[BUFSIZE]; size_t count = 5; count = read(0, buf, count); if (count == -1) { perror(“read failed”); exit(1); } count = write(1, buf, count); if (count == -1) { perror(“write failed”); exit(1); } fprintf(stderr, “n%zd bytes movedn”, count); chase$. /cat 5 1234 5 bytes moved chase$. /cat 5 123 TTY read returns the <enter> as ‘n’ char. And prints it. read syscall returns the number of bytes actually read. 4 bytes moved Any bytes not chase$. /cat 5 consumed by 123456 read are returned 12345 by the next read 5 bytes moved (in shell process). chase$ 6 -bash: 6: command not found chase$
Simple I/O: args and printf #include <stdio. h> int main(int argc, char* argv[]) { int i; printf("arguments: %dn", argc); for (i=0; i<argc; i++) { printf("%d: %sn", i, argv[i]); } } chase$ cc –o prog 1. c chase$. /forkexec prog 1 arguments: 1 0: prog 1 child 19178 exited with status 0 chase$. /forkexec prog 1 one 2 3 arguments: 4 0: prog 1 1: one 2: 2 3: 3 child 19181 exited with status 0
Environment variables (rough) #include <stdio. h> #include <stdlib. h> int main(int argc, char* argv[], char* envp[]) { int i; int count = atoi(argv[1]); for (i=0; i < count; i++) { printf("env %d: %sn", i, envp[i]); } }
Environment variables (rough) chase$ cc –o env 0. c chase$. /env 0 Segmentation fault: 11 chase$. /env 0 12 env 0: TERM_PROGRAM=Apple_Terminal env 1: TERM=xterm-256 color env 2: SHELL=/bin/bash env 3: TMPDIR=/var/folders/td/ng 76 cpqn 4 zl 1 wrs 57 hldf 1 vm 0000 gn/T/ env 4: Apple_Pub. Sub_Socket_Render=/tmp/launch-Ot. U 5 Bb/Render env 5: TERM_PROGRAM_VERSION=309 env 6: OLDPWD=/Users/chase/c 210 -stuff env 7: TERM_SESSION_ID=FFCE 3 A 14 -1 D 4 B-4 B 08… env 8: USER=chase env 9: COMMAND_MODE=unix 2003 env 10: SSH_AUTH_SOCK=/tmp/launch-W 03 wn 2/Listeners env 11: __CF_USER_TEXT_ENCODING=0 x 1 F 5: 0: 0 chase$
Environment variables (safer) #include <stdio. h> #include <stdlib. h> int main(int argc, char* argv[], char* envp[]) { int i; int count; if (argc < 2) { fprintf(stderr, "Usage: %s <count>n", argv[0]); exit(1); } count = atoi(argv[1]); for (i=0; i < count; i++) { if (envp == 0) { printf("env %d: nothing!n", i); exit(1); } else if (envp[i] == 0) { printf("env %d: null!n", i); exit(1); } else printf("env %d: %sn", i, envp[i]); } }
Where do environment variables come from? chase$ cc –o env. c chase$. /env chase$. /forkexec env Usage: env <count> child 19195 exited with status 1 chase$. /forkexec env 1 env 0: null! child 19263 exited with status 1 chase$
forkexec revisited char *lala = "lalalan"; char *nothing = 0; … main(int argc, char *argv[]) { int status; int rc = fork(); if (rc < 0) { … } else if (rc == 0) { argv++; execve(argv[0], argv, &lala); } else { … } chase$ cc –o fel forkexec-lala. c chase$. /fel env 1 env 0: lalala child 19276 exited with status 0 chase$
forkexec revisited again … main(int argc, char *argv[], char *envp[]) { int status; int rc = fork(); if (rc < 0) { … } else if (rc == 0) { argv++; execve(argv[0], argv, envp); } else { … } chase$ cc –o fe forkexec 1. c chase$. /fe env 3 env 0: TERM_PROGRAM=Apple_Terminal env 1: TERM=xterm-256 color env 2: SHELL=/bin/bash child 19290 exited with status 0 chase$
How about this? chase$. /fe fe fe env 3 <? ? ? >
How about this? chase$. /fe fe fe env 3 env 0: TERM_PROGRAM=Apple_Terminal env 1: TERM=xterm-256 color env 2: SHELL=/bin/bash It is easy for children to inherit child 19303 exited with status 0 environment variables from their child 19302 exited with status 0 parents. child 19301 exited with status 0 child 19300 exited with status 0 child 19299 exited with status 0 child 19298 exited with status 0 child 19297 exited with status 0 child 19296 exited with status 0 child 19295 exited with status 0 child 19294 exited with status 0 child 19293 exited with status 0 child 19292 exited with status 0 chase$ Exec* enables the parent to control the environment variables and arguments passed to the children. The child process passes the environment variables “to itself” but the parent program controls it.
- Slides: 88