88
D u k e S y s t e m s CPS 310 Unix Broadly Defined Jeff Chase Duke University http://www.cs.duke.edu/~chase/cps310

D u k e S y s t e m s CPS 310 Unix Broadly Defined Jeff Chase Duke University chase/cps310

Embed Size (px)

Citation preview

D u k e S y s t e m s

CPS 310Unix Broadly Defined

Jeff ChaseDuke University

http://www.cs.duke.edu/~chase/cps310

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

data dataPrograms

run asindependent processes.

Protected system calls

...and upcalls (e.g., signals)

Protected OS kernel

mediates access to

shared resources.

Threads enter the kernel for

OS services.

Each process has a private

virtual address space and one

thread.

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 parent fork child

wait exit

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.

int pid = wait*(&status);Wait for exit (or other status change) of a child, and “reap” its exit status.Recommended: use waitpid().

exec

parentprogram

initializes child

context

Unix fork/exit syscalls

parent

time

exit

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.

exit(status);Exit with status, destroying the process. Note: this is not the only way for a process to exit!

fork

child

data data

p

pid: 5587 pid: 5588

exit

fork

int pid;int status = 0;

if (pid = fork()) {/* parent */…..

} else {/* child */…..exit(status);

}

The fork syscall returns twice:

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

voiddofork(){ 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: forkdeepint 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}

main(int argc, char *argv[]){ count = atoi(argv[1]); dofork(); output pid}

How?

chase$ ./forkdeep 4 30866-> 30867 30867 30867-> 30868 30868 30868-> 30869 30869 30869-> 30870 30870 30870 30869 30868 30867 30866chase$

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

running

readyblocked

sleep

STOP wait

wakeup

dispatch

Before a thread can sleep, it must first enter the kernel via trap (syscall) or fault.

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 yield, it must enter the kernel, or the core must take an interrupt to return control to the kernel.

On entry to the running state, kernel code decides if/when/how to enter user mode, and sets up a suitable context.

yieldpreempt

Kernel Stacks and Trap/Fault Handling

data

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).

stack

stack

stack

stack

System calls and faults run

in kernel mode on a

kernel stack.

syscall dispatch

table

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.

Program

Running a program

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.

data

code (“text”)constants

initialized data Process

Thread

sections

segments

virtual memory

Unix: fork/exec

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 %d\n", rc,

WEXITSTATUS(status)); }}

A simple program: prog0

int main(){

}

chase$ cc –o forkexec forkexec.cchase$ cc –o prog0 prog0.cchase$ ./forkexec prog0child 19175 exited with status 0chase$

Unix fork/exec/exit/wait syscalls

fork parent fork child

wait exit

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.

int pid = wait*(&status);Wait for exit (or other status change) of a child, and “reap” its exit status.Recommended: use waitpid().

exec

parentprogram

initializes child

process context

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

Forkcall

Forkentry to

user space

Exit call

Forkreturn

Wait call

Waitreturnparent

child

transition from user to kernel mode (callsys)

transition from kernel to user mode (retsys)

Exec enters the child bydoctoring up a saved user context to “return” through.

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

+ +user ID

process IDparent PIDsibling links

children

virtual address space process descriptor (PCB)

resources

thread

stack

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.

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 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.

The Shell• Users may select from a range of command interpreter

(“shell”) programs available. (Or even write their own!)

– csh, sh, ksh, tcsh, bash: choose your flavor…

• Shells execute commands composed of program filenames, args, and I/O redirection symbols.– Fork, exec, wait, etc., 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

Process

Thread

Program

Virtual address space(Virtual Memory, VM)

kernel state

text

data

heap

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

Clone of parent VM.Environment (argv[] and envp[]) is configured by

parent program on exec.

Execsetup (ABI)

Unix: upcalls• The kernel directs control flow into user process at a fixed entry

point: e.g., entry for exec() is _crt0 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 data

Protected system calls

...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 data

Protected system calls

...and upcalls (signals)Kernel sends signals, e.g., to notify processes of faults.

Unix process view: data

Process

Thread

Program

SegmentsFiles

I/O channels(“file descriptors”)

stdinstdout

stderr

pipe

tty

socket

VM mappings

Labels: uid

text

data

anon VM

etc.

etc.

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

Process

Thread

ProgramFiles

I/O channels(“file descriptors”)

stdinstdout

stderr

pipe

tty

socket

A process has multiple channels for data movement in and out of the process (I/O).

The parent process and parent program set up and control the channels for a child (until exec).

Standard I/O descriptorsI/O channels

(“file descriptors”)

stdinstdout

stderrtty

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);}

Open files or other I/O channels are named within the process by an integer file descriptor value.

Standard descriptors forprimary 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.

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);

Files

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 spaceroot directory

mount point

user home directory

external media volume or network storage

applications etc.

Unix “file descriptors” illustrateduser space

pipe

file

socketper-processdescriptor

table

kernel space

system-wide open file table

tty

Disclaimer: this drawing is oversimplified

pointer

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.

int fd

Processes reference objects

Files

text

data

anon VM

Fork clones all references

Files

text

data

anon VM

Cloned references to VM segments are likely to be copy-on-write to create a lazy, virtual copy of the shared object.

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. What operations release refs?

Shell and child

dshtty

stdoutstderr

stdin dsh

tty

stdoutstderr

stdin

tty

stdoutstderr

stdinforktcsetpgr

pexecwait

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.

Child process inherits standard I/O channels to the terminal (tty).

1

2

3

Pipes

• 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.

pipe

int pfd[2] = {0, 0};pipe(pfd);/*pfd[0] is read, pfd[1] is write */

b = write(pfd[1], "12345\n", 6);b = read(pfd[0], buf, b);b = write(1, buf, b);

12345

A pipe is a bounded kernel buffer for passing bytes.

A key idea: Unix pipes

[http://www.bell-labs.com/history/unix/philosophy.html]

Unix programming environment

stdoutstdin

Standard unix programs read a byte stream from standard input (fd==0).

They write their output to standard output (fd==1).

That style makes it easy to combine simple programs using pipes or files.

If the parent sets it up, the program doesn’t even have to know.

Stdin or stdout might be bound to a file, pipe, device, or network socket.

The processes may run concurrently and are automatically synchronized

Shell pipeline example

Job

who grep

dsh

stdin stdout

stderr

tty

stderr

stdoutstderr

stdinchase$ who | grep chasechase console Jan 13 21:08 chase ttys000 Jan 16 11:37 chase ttys001 Jan 16 15:00 chase$

ttytty

stdout stdinttytty

pipe

But how to rewire the pipe?

C1 C2

stdin stdouttty

stdout stdintty

1 2P creates pipe. P

C1 closes the read end of the pipe, closes its stdout, “dups” the write end onto stdout, and execs.

P forks C1 and C2.Both children inherit both ends of the pipe, and stdin/stdout/stderr.Parent closes both ends of pipe after fork.

3A C2 closes the write end of the pipe, closes its stdin, “dups” the read end onto stdin, and execs.

3B

Unix dup* syscall

pipe outper-processdescriptor

table

dup2(fd1, fd2). What does it mean?

Yes, fd1 and fd2 are integer variables. But let’s use “fd” as shorthand for “the file descriptor whose number is the value of the variable fd”.

Then fd1 and fd2 denote entries in the file descriptor table of the calling process. The dup2 syscall is asking the kernel to operate on those entries in a kernel data structure. It doesn’t affect the values in the variables fd1 and fd2 at all!

int fd2 = 0

int fd1 = 5

pipe in

tty in

Hypothetical initial state before dup2(fd1, fd2) syscall.

tty out

Unix dup* syscall

pipe outper-processdescriptor

table

int fd2 = 0

int fd1 = 5

pipe in

tty in

Final state after dup2(fd1, fd2) syscall.

tty out

Then dup2(fd1,fd2) means: “close(fd2), then set fd2 to refer to the same underlying I/O object as fd1.”

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(fd1).

X>

Unix dup* syscall

pipe outper-processdescriptor

table

int fd2 = 0

int fd1 = 5

pipe in

tty in

Final state after dup2(fd1, fd2) syscall.

tty out

Then dup2(fd1,fd2); close(fd1) means: “remap the object referenced by file descriptor fd1 to fd2 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 fd1 or fd2. Also, changing the values in fd1 and fd2 can never affect the state of the entries in the file descriptor table. Only the kernel can do that.

X

Simpler example

Feeding a child through a pipe

parent

stdin

stderr

stdoutstdin

stderr

stdout

child cid

Parentint ifd[2] = {0, 0};pipe(ifd);cid = fork();

Parentclose(ifd[0]);count = read(0, buf, 5);count = write(ifd[1], buf, 5);waitpid(cid, &status, 0);printf("child %d exited…”);

Childclose(0);close(ifd[1]);dup2(ifd[0],0);close(ifd[0]);execve(…);

chase$ man dup2chase$ cc -o childin childin.cchase$ ./childin cat512345123455 bytes movedchild 23185 exited with status 0chase$

pipe

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

reality

concept

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.

MapReduce/Hadoop

MapReduce 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.

Dataflow programming

Hadoop

Sockets

• 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 socket is a buffered channel for passing data over a network.

socket

clientint 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, ….);

A simple, familiar example

“GET /images/fish.gif HTTP/1.1”

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);

request

reply

client (initiator) server

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 1960s).

– 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.

Other application programs

cc

Other application programs

Hardware

Kernel

sh who a.out

date

wc

grepedvi

ld

as

comp

cppnroff

Unix defines uniform, modular ways to combine programs to build up more complex functionality.

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.”

The UNIX Time-Sharing System* D. M. Ritchie and K. Thompson

1974

DEC PDP-11/24

http://histoire.info.online.fr/pdp11.html

Let’s pause a moment to reflect...

From Hennessy and Patterson, Computer Architecture: A Quantitative Approach, 4th edition, 2006

Core Rate(SPECint)

Notelog 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. Windows2. Window server3. App events4. Widget toolkit

Unix, looking backward: security

• Presumes multiple users sharing a machine.

• Each user has a userID.– UserID owns all files created by all programs user runs.

– Any program can access any file owned by userID.

• 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

fg

bgstop

exit

exit

tty intty out

SIGCONT

SIGCONTset-fg ctrl-z

(SIGTSTP)

STOP

EXITfork + set-fg

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.

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)

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.

How to seize control of the terminalif (tcsetpgrp(0, getpid()) < 0) {

perror("tcsetpgrp failure"); exit(EXIT_FAILURE);

}

You shouldn’t have to know

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

Job 1

Job 2

Job 3

P1A P1B

P2A

P3A

dshAny of these child jobs/processes could

exit or stop at any time.

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

fork

exec

EXIT

“echo y&”

dsh

“jobs”

wait

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?”

Parent can use a non-blocking wait syscall to poll (query) the

status of a child.

waitpid(pid, &status, WNOHANG);

Monitoring background jobs

fork

exec

EXIT

“echo y&”

dsh

Parent is waiting for read to return the next input command.

How should parent learn of the child exit event?

coffee….

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?

A “real” shell should notice and inform the user immediately. But

dsh will not.

Monitoring background jobs

fork

exec

EXIT

“echo y&”

dsh

SIGCHLD

Event notificationsSTOP

SIGCHLD

coffee….

A “real” shell receives SIGCHLD signals from the kernel when a

child changes state.

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.

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 moved\n”, count);

chase$ cc –o cat5 cat5.cchase$ ./cat5<waits for tty input>12345<enter>123455 bytes movedchase$

tty input:output:

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 moved\n”, count);

chase$ ./cat512341234

5 bytes movedchase$ ./cat5123123

4 bytes movedchase$ ./cat5123456123455 bytes movedchase$ 6-bash: 6: command not foundchase$

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 moved\n”, count);

chase$ ./cat512341234

5 bytes movedchase$ ./cat5123123

4 bytes movedchase$ ./cat5123456123455 bytes movedchase$ 6-bash: 6: command not foundchase$

TTY read returns the <enter> as ‘\n’ char.And prints it.

read syscall returns the number of bytes actually read.

Any bytes not consumed by read are returned by the next read(in shell process).

Simple I/O: args and printf

#include <stdio.h>

int main(int argc, char* argv[]){ int i;

printf("arguments: %d\n", argc); for (i=0; i<argc; i++) { printf("%d: %s\n", i, argv[i]); }}

chase$ cc –o prog1 prog1.cchase$ ./forkexec prog1arguments: 10: prog1child 19178 exited with status 0chase$ ./forkexec prog1 one 2 3arguments: 40: prog11: one2: 23: 3child 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: %s\n", i, envp[i]); }}

Environment variables (rough)chase$ cc –o env0 env0.cchase$ ./env0Segmentation fault: 11chase$ ./env0 12env 0: TERM_PROGRAM=Apple_Terminalenv 1: TERM=xterm-256colorenv 2: SHELL=/bin/bashenv 3: TMPDIR=/var/folders/td/ng76cpqn4zl1wrs57hldf1vm0000gn/T/env 4: Apple_PubSub_Socket_Render=/tmp/launch-OtU5Bb/Renderenv 5: TERM_PROGRAM_VERSION=309env 6: OLDPWD=/Users/chase/c210-stuffenv 7: TERM_SESSION_ID=FFCE3A14-1D4B-4B08…env 8: USER=chaseenv 9: COMMAND_MODE=unix2003env 10: SSH_AUTH_SOCK=/tmp/launch-W03wn2/Listenersenv 11: __CF_USER_TEXT_ENCODING=0x1F5:0:0chase$

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: %s\n", i, envp[i]); }}

Where do environment variables come from?

chase$ cc –o env env.cchase$ ./envchase$ ./forkexec envUsage: env <count>child 19195 exited with status 1chase$ ./forkexec env 1env 0: null!child 19263 exited with status 1chase$

forkexec revisited

char *lala = "lalala\n";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.cchase$ ./fel env 1env 0: lalala

child 19276 exited with status 0chase$

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 forkexec1.cchase$ ./fe env 3env 0: TERM_PROGRAM=Apple_Terminalenv 1: TERM=xterm-256colorenv 2: SHELL=/bin/bashchild 19290 exited with status 0chase$

How about this?

chase$ ./fe fe fe fe fe fe fe fe fe fe fe fe env 3

<???>

How about this?chase$ ./fe fe fe fe fe fe fe fe fe fe fe fe env 3env 0: TERM_PROGRAM=Apple_Terminalenv 1: TERM=xterm-256colorenv 2: SHELL=/bin/bashchild 19303 exited with status 0child 19302 exited with status 0child 19301 exited with status 0child 19300 exited with status 0child 19299 exited with status 0child 19298 exited with status 0child 19297 exited with status 0child 19296 exited with status 0child 19295 exited with status 0child 19294 exited with status 0child 19293 exited with status 0child 19292 exited with status 0chase$

It is easy for children to inherit environment variables from their parents.

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.