This chapter describes processes and the library functions that operate on them.
Executing a command, starts a process that is numbered and tracked by the operating system. Processes are always generated by other processes. For example, log in to your system running a shell, then use an editor such as vi. Take the option of invoking the shell from vi. Execute the ps command and you will see a display resembling this (which shows the results of a ps -f command):
UID |
PID |
PPID |
C |
STIME |
TTY |
TIME |
COMD |
---|---|---|---|---|---|---|---|
abc |
24210 |
1 |
0 |
06:13:14 |
tty29 |
0:05 |
-sh |
abc |
24631 |
24210 |
0 |
06:59:07 |
tty29 |
0:13 |
vi c2 |
abc |
28441 |
28358 |
80 |
09:17:22 |
tty29 |
0:01 |
ps -f |
abc |
28358 |
24631 |
2 |
09:15:14 |
tty29 |
0:01 |
sh -i |
User abc has four processes active. The process ID (PID) and parent process ID (PPID) columns show that the shell started when user abc logged on is process 24210; its parent is the initialization process (process ID 1). Process 24210 is the parent of process 24631, and so on.
A program may need to run one or more other programs based on conditions it encounters. Reasons that it might not be practical to create one large executable include:
You might want to execute two, or more, of the modules concurrently.
The load module might get too big to fit in the maximum process size for your system.
You might not have control over the object code of all the other modules you want to include.
The "fork(2)" and "exec(2) " functions let you create a new process (a copy of the creating process) and start a new executable in place of the running one.
The functions listed in Table 3-1 are used to control user processes:
Table 3-1 Process Functions
Function Name |
Purpose |
---|---|
fork |
Create a new process |
exec execl execv execle execve execlp execvp |
Execute a program |
|
|
|
|
|
|
|
|
exit _exit |
Terminate a process |
wait |
Wait for a child process to stop or terminate |
dladdr |
Translate address to symbolic information |
dlclose |
Close a shared object |
dlerror |
Get diagnostic information |
dlopen |
Open a shared object |
dlsym |
Get the address of a symbol in a shared object |
setuid setgid |
Set user and group IDs |
setpgrp |
Set process group ID |
chdir fchdir |
Change working directory |
chroot |
Change root directory |
nice |
Change priority of a process |
getcontext setcontext |
Get and set current user context |
getgroups setgroups |
Get or set supplementary group access list IDs |
getpid getpgrp getppid getpgid |
Get process, process group, and parent process IDs |
|
|
|
|
getuid geteuid getgid getegid |
Get real user, effective user, real group, and effective group IDs |
|
|
|
|
pause |
Suspend process until signal |
priocntl |
Control process scheduler |
setpgid |
Set process group ID |
setsid |
Set session ID |
waitid |
Wait for a child process to change state |
The fork call creates a new process that is an exact copy of the calling process. The new process is the child process; the old process is the parent process. The child gets a new, unique process ID. The fork function returns a 0 to the child process and the child's process ID to the parent. The returned value is how a forked program determines whether it is the parent process or the child process.
The new process created by the fork or exec function inherits all open file descriptors from the parent including the three standard files: stdin, stdout, and stderr. When the parent has buffered output that should appear before output from the child, the buffers must be flushed before the fork.
The following code is an example of a call to fork and the subsequent actions:
pid_t pid; pid = fork; switch (pid) { case -1: /* fork failed */ perror ("fork"); exit (1); case 0: /* in new child process */ printf ("In child, my pid is: %d\n", getpid(); ); do_child_stuff(); exit (0); default: /* in parent, pid contains PID of child */ printf ("In parent, my pid is %d, my child is %d\n", getpid(), pid); break; } /* Parent process code */ ...
If the parent and the child process both read input from a stream, whatever is read by one process is lost to the other. So, once something has been delivered from the input buffer to a process, the buffer pointer has moved on.
An obsolete practice is to use fork() and exec() to start another executable, then wait for the new process to die. In effect, a second process is created to perform a subroutine call. It is much more efficient to use dlopen(), dlsym(), and dlclose() as described in "Runtime Linking " to make a subroutine temporarily resident in memory.
exec is the name of a family of functions that includes execl, execv, execle, execve, execlp, and execvp. All load a new process over the calling process, but with different ways of pulling together and presenting the arguments of the function. For example, execl could be used like this
execl("/usr/bin/prog2", "prog2", progarg1, progarg2, (char (*)0));
The execl argument list is:
/usr/bin/prog2 |
The path name of the new program file. |
prog2 |
The name the new process gets in its argv[0]. |
progarg1, progarg2 |
The arguments to prog2 as char (*)s. |
(char (*)0) |
A null char pointer to mark the end of the arguments. |
See execl(2) for more details.
There is no return from a successful execution of any variation of exec(); the new process overlays the process that calls exec. The new process also takes over the process ID and other attributes of the old process. If a call to exec fails, control is returned to the calling program with a return value of -1. You can check errno to learn why it failed.
An application can extend its address space during execution by binding to additional shared objects. There are several advantages in this delayed binding of shared objects:
Processing a shared object when it is required, rather than during the initialization of an application, may greatly reduce start-up time. Also, the shared object may not be required during a particular run of the application, for example, objects containing help or debugging information.
The application may choose between a number of different shared objects depending on the exact services required; for example, networking protocols.
Any shared objects added to the process address space during execution may be freed after use.
The following is a typical scenario that an application may perform to access an additional shared object:
A shared object is located and added to the address space of a running application using dlopen(3X). Any dependencies of shared object are also located and added at this time. For example:
#include <stdio.h> #include <dlfcn.h> main(int argc, char ** argv) { void * handle; ..... if ((handle = dlopen("foo.so.1", RTLD_LAZY)) == NULL) { (void) printf("dlopen: %s\n", dlerror()); exit (1); } .....
The added shared objects are relocated, and any initialization sections in the new shared objects are called.
The application locates symbols in the added shared objects using dlsym(3X). The application can then reference the data or call the functions defined by these new symbols. Continuing the preceding example:
if (((fptr = (int (*)())dlsym(handle, "foo")) == NULL) || ((dptr = (int *)dlsym(handle, "bar")) == NULL)) { (void) printf("dlsym: %s\n", dlerror()); exit (1); }
After the application has finished with the shared objects the address space is freed using dlclose(3X). Any termination sections within the shared objects being freed are called at this time. For example:
if (dlcose (handle) != 0) { (void) printf("dlclose: %s\n", dlerror()); exit (1); }
Any error conditions that occur as a result of using these runtime linker interface routines can be displayed using dlerror(3X).
The services of the runtime linker are defined in the header file dlfcn.h and are made available to an application via the shared library libdl.so.1. For example:
$ cc -o prog main.c -ldl
Here the file main.c can refer to any of the dlopen(3X) family of routines, and the application prog will be bound to these routines at runtime.
For a thorough discussion of application directed runtime linking, see the Linker and Libraries Guide. See dladdr(3X), dlclose(3X), dlerror(3X), dlopen(3X), and dlsym(3X) for use details.
The UNIX system scheduler determines when processes run. It maintains process priorities based on configuration parameters, process behavior, and user requests. It uses these priorities to assign processes to the CPU.
Scheduler functions give users varying degrees of control over the order in which certain processes run and the amount of time each process may use the CPU before another process gets a chance.
By default, the scheduler uses a time-sharing policy. A time-sharing policy adjusts process priorities dynamically in an attempt to give good response time to interactive processes and good throughput to CPU-intensive processes.
The scheduler also provides an alternate real-time scheduling policy. Real-time scheduling allows users to set fixed priorities--priorities that the system does not change. The highest priority real-time user process always gets the CPU as soon as it can be run, even if other system processes are also eligible to be run. A program can therefore specify the exact order in which processes run. You can also write a program so that its real-time processes have a guaranteed response time from the system.
For most Solaris 2.x system environments, the default scheduler configuration works well and no real-time processes are needed: administrators need not change configuration parameters and users need not change scheduler properties of their processes. However, for some programs with strict timing constraints, real-time processes are the only way to guarantee that the timing requirements are met.
For more information, see priocntl(1), priocntl(2) and dispadmin(1M) of the man Pages(2): System Calls. For a fuller discussion of this subject, see Chapter 4, Process Scheduler ."
Functions that do not conclude successfully almost always return a value of -1 to your program. (For a few functions in Section 2 of the man Pages(2): System Calls, there are calls for which no return value is defined, but these are the exceptions.) In addition to the -1 that is returned to the program, the unsuccessful function places an integer in an externally declared variable, errno. In a C program, you can determine the value in errno if your program contains the following statement.
#include <errno.h>
The value in errno is not cleared on successful calls, so check it only if the function returns -1. Since some functions return -1 but do not set errno refer to the man page for the function to be sure that errno contains a valid value. See error descriptions in intro(2) of the 1.
Use the C language function perror(3C) to print an error message on stderr based on the value of errno.