Overview
This article mainly describes the process of Linux under the relevant operations, and later will write an article on the Linux thread operation. These two articles and I have to finish an article (IPC communication under Linux) to form a complete series, can be said that the first two articles is the foreshadowing and foundation of the third article.
Part I: Fork creation process
================================================
First, let's look at a fork () example of creating a child process.
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
extern void Error_exit ( char*);
extern void Success_exit (char*);
int main (int argc, char* argv[])
{
pid_t child_pid;
switch (child_pid = fork ()) {
case-1:
error_exit ("fork failed.");
break;
Case 0: Sleep
(5);
Success_exit ("Child Exit");
break;
Default:
if (wait (NULL) = 1) {
error_exit ("Wait failed."),
else {
success_exit ("Child has exit.");
}
break;
return 0;
}
void Error_exit (char* msg)
{
printf ("NG:%s\n", msg);
Exit ( -1);
}
void Success_exit (char* msg)
{
printf ("OK:%s\n", msg);
Exit (0);
}
The above example is the simplest, the child process failed to exit, otherwise the subprocess waits 5 seconds and prints "child exit" and exits, and the parent process waits for the subprocess to exit and then exits the "Child has exit". As for the relationship between the fork and the parent-child process, here are the following points to note:
1. Fork has three types of return values: Failure return-1; When successful, the subprocess returns 0; the parent process returns the PID of the child process.
2. Theoretically, the subprocess created by Fork inherits (copies) a copy of the data segments, stacks, and snippets of the parent process. In fact, to avoid waste, fork does not immediately make any copies of the child process. For code snippets, fork will point the process-level page table entry for the subprocess to the same physical memory page frame as the parent process, and the write-time replication technique for the data segment and the stack segment. This is very meaningful because many times the child process is created not so that the child process and the parent process run the same code at the same time, but instead run other code through exec.
3. It is also important to note that the parent-child process fully shares the file offset and file status flag of the file that was opened before fork. To be blunt, if the child process does not perform the exec operation, the parent-child process writes to the file opened before fork, but is mixed together. So if you don't want this to happen, you need to synchronize the parent-child process.
4. When the subprocess exits, the system automatically reclaims the memory resources of the child process. With this feature, we can extract some complex operations and create a subprocess to specifically perform this operation, eventually passing the results to the parent process via file, pipeline, or other interprocess communication technology. This should be a very valuable skill, suitable for the operation of frequent and complex memory operation occasions. Using a subprocess to do this you don't need to worry about memory leaks and the problem of excessive heap memory fragmentation.
5. Do not make any assumptions about the order in which the parent-child process is executed after fork (although the kernel version of Linux has been normalized for this), it is best to explicitly qualify their execution order, which can be achieved by synchronizing technology (semaphores, file locks, pipe messages, signals, and wait () functions). In the example above, there are two ways to change the execution order of a parent-child process, one is sleep (), and one is wait (). It is important to note that sleep () is not a canonical method, in contrast to wait () is much better, even if the child process sleeps for 5 seconds, the parent process is still effectively waiting for the child process to print the message and exit first.
Here's another example of using signal synchronization:
#include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <signal.h> #include <
errno.h> #define SYNC_SIG SIGUSR1 extern void Error_exit (char*);
extern void Success_exit (char*);
static void handler (int sig) {return;}
int main (int argc, char* argv[]) {pid_t child_pid;
sigset_t Block_mask,old_mask,empty_mask;
struct Sigaction sa;
Set process ' s signal mask as Sync_sig sigemptyset (&block_mask);
Sigaddset (&block_mask,sync_sig);
if (Sigprocmask (sig_block,&block_mask,&old_mask) = = 1) error_exit ("process mask failed.");
Set handler for Sync_sig Sigemptyset (&sa.sa_mask);
Sa.sa_flags = Sa_restart;
Sa.sa_handler = handler;
if (sigaction (sync_sig,&sa,null) = = 1) error_exit ("Set Sigaction failed.");
switch (child_pid = fork ()) {case-1: Error_exit ("fork failed.");
Break
Case 0:sleep (5); Child Send Sync_sig to parent.
printf ("Child:send signal.\n");
Kill (Getppid (), Sync_sig);
Success_exit ("Child Exit");
Break
Default://suspend parent to wait signale from child.
printf ("Parent:suspend process.\n");
Sigemptyset (&empty_mask);
if (sigsuspend (&empty_mask) = = 1 && errno!= eintr) error_exit ("Parent:suspend failed");
printf ("Parent:sync_sig received,restart parent.\n");
Wait for child exit A.
if (wait (NULL) = 1) error_exit ("Wait child failed.");
Success_exit ("Child has exit.");
Break
return 0;
} void Error_exit (char* msg) {printf ("NG:%s\n", msg);
Exit (-1);
} void Success_exit (char* msg) {printf ("OK:%s\n", msg);
Exit (0); }
Note that this example adds a signal synchronization based on the previous example. In fact there are two synchronizations in this example, the first of which is the synchronization of the signal, the purpose of which is to ensure that part of the operation in the child process (such as the Sleep here (5), of course, you can replace it with any action you want to perform. is performed before certain operations in the parent process. The second synchronization is implemented by wait, and his role is to ensure that the child process exits before the parent process, which prevents the subprocess from becoming an orphan process.
Part Two: termination of the process
================================================
With regard to the termination of the process, the main functions involved are _exit (), exit (), Atexit (), On_exit (). The following points are described below for process termination:
1. The termination of the process is divided into two types: normal and abnormal. The exception termination may be caused by some signals, some of which may also cause the process to produce a core dump file.
2. Normal termination can be achieved by system call _exit () or the GNU C standard library function exit (). Both have a status argument that represents the function exit value. The usual return function has the same effect as the exit () function (this means that, in the case of a parameter, returns with no arguments depend on the version standard of the C language and the compiler used).
3. The kernel performs multiple cleanup steps, whether the process is normal or abnormally terminated. Unlike system call _exit (), calling the Library function exit () to terminate a process normally terminates the execution of an exit handler registered with the GNU C library function atexit () or On_exit () (These exit handlers call the function _exit () Or it will not be executed because of the termination of the signal. ), and then refresh the stdio buffer.
4. The question of refreshing the buffer is also more interesting. Let's look at the following example:
If you run the following code directly, the results of the output are as follows:
[Root@brandy process]#./process_test_stdio
Hello world!
Ysy
When you redirect the output to a file, the output results are as follows:
[Root@brandy process]#./process_test_stdio > Log
[Root@brandy process]# cat log
ysy
Hello world!
Hello world!
The code is as follows:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
extern void Error_exit ( char*);
extern void Success_exit (char*);
int main (int argc, char* argv[])
{
printf ("Hello world!\n");
Write (Stdout_fileno, "ysy\n", 4);
if (fork () = = 1)
error_exit ("fork failed.\n");
return 0;
}
void Error_exit (char* msg)
{
printf ("NG:%s\n", msg);
Exit ( -1);
}
void Success_exit (char* msg)
{
printf ("OK:%s\n", msg);
Exit (0);
}
Why is the result different? This is because the first case standard output is directed to the terminal, which defaults to line buffering, so the fork directly outputs the contents of the buffer "Hello world!". In the second case, the output is defined to the file, the default is a block buffer, when printf content is temporarily stored in the standard output buffer area, with the execution of Fork, the child process copies the standard output buffer, resulting in output two times "Hello world!." As for why "Ysy" ran to the front, and only once. This is because the write () system call passes the data directly to the kernel cache, and fork does not copy the buffer. And when written to a file, the kernel cache is written to the file, while the standard output is not written to the file until the standard output buffer is refreshed at the end of the process.
To avoid the confusion caused by the timing of brush buffers, users can call the function fflush () to refresh the buffer or use SETVBUF () and setbuf () to turn off the buffering function of the standard output. You can also use _exit () to exit in a child process as much as possible, because _exit () does not refresh the buffer of the standard output, so that at least the above problem does not result in the output of the "Hello world!" two times.
Part III: Monitoring of the process
================================================
This section is about the parent process's monitoring actions on the child processes. It mainly involves three knowledge points: the necessity of monitoring the sub process (i.e. its purpose), the system call Wait () and its related call, and the processing of the SIGCHLD signal. The latter two knowledge points are the monitoring means of the parent process's child processes.
I. The necessity of monitoring sub-processes
Many times the parent process needs to monitor the status of the child process, and there are several things to declare:
1. Synchronization between parent-child processes, and checking whether the child process ends properly. For example, some operations of the parent process require the end of a subprocess to execute, and sometimes the parent process needs to get information such as the exit status of the subprocess, which requires the child process to be monitored.
2. Avoid the mass production of zombie processes. At the end of the subprocess, the kernel converts the ending subprocess into a zombie process (with reference to the zombie process and its hazards) before the parent process calls the wait () or waitpid () function. After the parent process invokes a function such as wait () or waitpid (), the kernel clears the completed subprocess completely, otherwise leaving behind a large number of zombie processes at the end of the parent process.
3. The avoidance of orphan processes, while not having a significant impact on the system as in the case of zombies, does not recommend that the parent process terminate execution regardless of the child process state without authorization.
second, wait () and its related calls
This section talks only about Wait () and Waitpid (), which are both system calls that can be used to monitor the state of a child process. Their definition is as follows:
#include <sys/types.h>
#include <sys/wait.h>
pid_t Wait (int *status);
pid_t waitpid (pid_t pid, int *status, int options);
For the relationship between the two calls, make the following remarks:
1. Both have a status parameter to return the child process termination state.
2. Both have a return value of type pid_t that represents the PID of the child process being monitored.
3. Error returns-1, if errno is set to echild means no child processes can wait, that is, all child processes of the parent process have ended and the parent process has acquired the end state.
4. Wait () a child process that only waits in order for the end (for example, if more than one child process has ended before the call is made, the wait will only return the termination state of one subprocess at a time, in the same order as the ending order of the child process). , while Waitpid can select the wait mode through parameter PID (pid>0 wait for the specified subprocess; pid=0 wait for all child processes of the same process group as the calling process; Pid=-1 wait for all child processes; pid<- 1 wait for the process group identifier to be equal to the PID absolute value of all child processes).
5. The wait () is a blocking wait until a subprocess is finished before returning, and Waitpid () can specify the waiting mode through the parameter options (wuntraced: Returns the aborted subprocess and the child process information stopped by the signal) ; wcontinued: Returns the state information of the stopped child process that was executed due to sigcont signal recovery; Wnohang: Returns immediately without blocking if the state of the specified waiting subprocess has not changed.
Whether it is wait () or waitpid (), the status they return can be resolved by a standard set of macros defined in the header file <sys/wait.h> (the names of these macros are well written: wifexited (status); normal end Wifsignaled (status): Killed by the signal; wifstopped (status): stopped by the signal; wifcontinued (status): The signal was stopped after the signal Sigcont resumed execution). Only one macro will return a true value after each returned status resolution.
Here is an example that produces a subprocess and returns the state transition process of a subprocess at all times.
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/wait.h> #include <
string.h> extern void Error_exit (char*);
extern void Success_exit (char*);
extern void Print_wait_status (const char*, int);
int main (int argc,char* argv[]) {int status;
pid_t Child_pid;
Switch (fork ()) {case-1: Error_exit ("fork failed!");
Case 0:printf ("child:started with pid=%d.\n", Getpid ()); if (argc > 1)/* Exit with the ARG supplied on command line.
* * Exit (Atoi (argv[1)); else/* Wait for signal, until get kill signal.
* for (;;)
Pause ();
Error_exit ("Nerver is here."); Default:for (;;) {/* Chek child's status until child exit. */Child_pid = Waitpid ( -1,&status,wuntraced|
wcontinued);
if (child_pid = = 1) error_exit ("Waitpid failed!"); printf ("Father:waitpid returned,child_pid=%ld,status=0x%04x (%d,%d). \ n ", (Long) child_pid, (unsigned int) status,status >> 8,status & 0xff);
Print_wait_status (Null,status); if (wifexited (status) | |
wifsignaled (status)) Success_exit ("Child has exited");
} return 0;
} void Error_exit (char* msg) {printf ("NG:%s\n", msg);
Exit (-1);
} void Success_exit (char* msg) {printf ("OK:%s\n", msg);
Exit (0);
} void Print_wait_status (const char* msg, int status) {if (msg!= NULL) printf ("%s\n", msg);
if (wifexited (status)) {printf ("Child exited:status=%d.\n", Wexitstatus (status));
else if (wifsignaled (status)) {printf ("Child killed by signal:%d\n", Wtermsig (status));
else if (wifstopped (status)) {printf ("Child stopped by signal:%d\n", Wstopsig (status));
else if (wifcontinued (status)) {printf ("Child continued.\n");
else {printf ("Unknow status.\n"); }
}
In this example, if the command line already has an argument, the child process exits immediately with this argument.
[Root@brandy process]#./process_test_waitpid 9
child:started with pid=21109.
Father:waitpid returned,child_pid=21109,status=0x0900 (9,0).
Child exited:status=9.
Ok:child has exited
If you do not specify a command-line argument, the child process invokes the pause wait signal. The parent process monitors the status change of the child process at any time, and immediately outputs the status change information of the subprocess as soon as there is a change, until the child process is finished.
[root@brandy process]#./process_test_waitpid & &NBSP;&NBSP;[1
] 21111 [root@brandy process]# child:started with pid=21112. [root@brandy process]# kill-stop 21112 [root@brandy Pro
cess]# father:waitpid returned,child_pid=21112,status=0x137f (19,127). child stopped by signal:19 [root@brandy process]# Kill -18 21112 [root@brandy process]# father:waitpid returned,child_pid=21112,status=
0xFFFF (255,255).
child continued. [root@brandy process]# kill-15 21112 [root@brandy Proce
ss]# father:waitpid returned,child_pid=21112,status=0x000f (0,15). child killed by signal:15 ok:chilD has exited [1]+ done &NBS P ./process_test_waitpid [root@brandy process]#
third, SIGCHLD signal
The wait and Waitpid calls described earlier may also be able to monitor the subprocess, but there are some drawbacks. For example, most of the cases are blocking mode, although waitpid can set options for Wnohang polling, or it will cause waste of CPU resources. What to do, SIGCHLD can help us solve this problem.
At the end of the child process, the system sends a SIGCHLD signal to its parent process. So we can also detect this signal to determine whether to call wait or waitpid to return the child process exit state and to clean up the zombie process, that is, in the signal SIGCHLD handler call wait or Waitpid call. However, it is important to set a signal SIGCHLD handler to emphasize that :
1. When the signal handler is invoked, the system will temporarily block the signal that caused the call, and will not queue the standard signal such as SIGCHLD. So the problem is, if there are two of them, and our signal handler calls only one wait () or waitpid () call, then there may be a zombie process that can not be cleaned up. So it's best to use the following loops in the SIGCHLD signal handler to ensure that all the finished processes are cleaned up.
while (Waitpid ( -1, Null,wnohang) > 0)
continue;
2. There is also a problem, if the child process created by the SIGCHLD signal processing function before the end of what to do, but also the smooth detection of sigchld it. This is not the same according to the implementation of different systems, at least SUSV3 does not provide for this. Standard practice is to set up the SIGCHLD signal processing function before the child process is created.
3. When the child process stops, the system also sends SIGCHLD to the parent process, and if you want the parent process to ignore this, you can pass in the SA_NOCLDSTOP flag when sigaction () sets the SIGCHLD signal processing function. The system does not emit a SIGCHLD signal because the child process stops.
4. If the parent process is not interested in the exit status of the subprocess, but simply wants to clean up the zombie process, it is simply that we only need to explicitly set the SIGCHLD processing to sig_ign (although the default system is also ignoring the SIGCHLD signal, but explicitly specified as Sig_ The behavior of the IGN is not the same. The system then deletes the aborted subprocess directly and does not turn into a zombie process.
I've also written an example of Waitpid (), which is an example of a loop called waitpid () in the parent process until the child process exits. The following example calls Waitpid () in the processing function of the signal sigchld, and the parent process calls Sigsuspend () to block up and wait for the child process to send sigchld.
The test results on Linux are as follows: The number of parameters determines the number of child processes and the time delay of the child process.
[Root@brandy process]#./PROCESS_TEST_SIGCHLD 2 3 7
child-01:pid = 32722 start.
Child-02:pid = 32723 start.
Child-03:pid = 32724 start.
Child-01:pid = 32722 exiting.
Handler:signal sigchld Accept.
handler:child_pid=32721, status=0x0000 (0,0) child exited:status=0.
Child-02:pid = 32723 exiting.
Child-03:pid = 32724 exiting.
Handler:return.
Handler:signal sigchld Accept.
handler:child_pid=32721, status=0x0000 (0,0) child exited:status=0.
handler:child_pid=32721, status=0x0000 (0,0) child exited:status=0.
Handler:return.
end:forked 3 child; Accept 2 Time SIGCHLD
The code is as follows:
<pre name= "code" class= "CPP" ><pre name= "code" class= "plain" > #include <stdio.h> #include <
signal.h> #include <unistd.h> #include <errno.h> #include <sys/wait.h> #include <stdlib.h>
void Sigchld_handler (int);
void Print_wait_status (const char*, int);
static int num_alive_child = 0;
int main (int argc, char* argv[]) {int j,num_child = 0,num_sig = 0;
pid_t Child_pid;
sigset_t Block_mask,empty_mask;
struct Sigaction sa; Setbuf (stdout, NULL);
/* Disable buffering of stdout * * 1:set handler for SIGCHLD/Sigemptyset (&sa.sa_mask);
sa.sa_flags = 0;
Sa.sa_handler = Sigchld_handler;
if (sigaction (sigchld,&sa,null) = = 1) {printf ("err:sigaction failed.\n");
Exit (-1);
}/* 2:block signal SIGCHLD * * Sigemptyset (&block_mask);
Sigaddset (&BLOCK_MASK,SIGCHLD); if (Sigprocmask (sig_setmask,&block_mask,null) = =-1) {printf ("Err:sigprocmask faileD.\n ");
Exit (-1);
}/* Fork child process */for (j=1;j<argc;j++) {num_alive_child++;
switch (child_pid = fork ()) {case-1: num_alive_child--; /* kill exist child and then exit'll be better.
* * _EXIT (-1);
Case 0:printf ("child-%02d:pid =%ld start.\n", J, (Long) getpid ());
Sleep ((int) atoi (argv[j]);
printf ("Child-%02d:pid =%ld exiting.\n", J, (Long) getpid ());
_exit (0);
default:num_child++;
Break
}/* Wait for signal sigchld from child */Sigemptyset (&empty_mask); while (Num_alive_child > 0) {if (Sigsuspend (&empty_mask) = = 1 && errno!= eintr) {Prin
TF ("Err:sigsuspend failed.\n");
Exit (-1);
} num_sig++; printf ("end:forked%d child;
Accept%d time sigchld\n ", Num_child,num_sig);
Exit (0); } void Sigchld_handlER (int sig) {int status, Old_errno;
pid_t Child_pid;
Old_errno=errno;
printf ("Handler:signal sigchld accept.\n"); /* 3:wait All exited the child process * * (Child_pid = waitpid ( -1,&status,wnohang)) > 0) {printf ("Ha Ndler:child_pid=%ld, status=0x%04x (%d,%d) ", (long) Getpid (), (unsigned int) status, status >> 8, Statu
S & 0xff);
Print_wait_status (Null,status);
num_alive_child--;
} if (child_pid = = 1 && errno!= echild) {printf ("Handler:waitpid failed,errno=%d.\n", errno);
Exit (-1);
Sleep (5);
printf ("handler:return.\n");
errno = Old_errno; }
In this example, the focus of the previous emphasis on the design of SIGCHLD signal processing functions is fully embodied. Refer to the comment number in the code, which is described again as follows:
1. The function starts with a signal processing function to avoid the completion of the child process before the signal processing function is set.
2. Block the signal sigchld before the subprocess is created to prevent the child from ending between checking the Num_aliave_child and executing sigsuspend (). If the child process sends a signal SIGCHLD to the parent process during this time, it will cause the parent process to always block in sigsuspend () because no child job may have sent the signal sigchld.
3. In the SIGCHLD signal processing function, the rotation method is used to invoke waitpid () to handle all the finished child processes. Because when the signal processing function is executed, the system blocks SIGCHLD and does not queue it, so doing so can be a good way to recycle the child processes that end when the signal processing function is executed.
4. There is also a point in the above example where the parent process can receive a SIGCHLD signal when a child process is paused. If you want to avoid this situation, you can specify the SA_NOCLDSTOP flag when calling Sigaction ().
Part IV: Implementation of the new program Execve ()
================================================
The EXECVE () call allows the new program to be loaded into the memory space of a process, based on which the C language provides the Exec series library functions. Here are some instructions for the calls loaded by the new program:
1. The EXEC () series functions have Execve (), Execle (), EXECLP (), EXECVP (), Execv (), Execl (), and altogether 6 functions. Their usage can be accessed from the Man manual, but the naming is regular, which helps to memorize its usage.
Description of the program file (-,p):
The representation program file with P does not need a full path when specified, and the system automatically searches according to the path path. such as EXECLP () and EXECVP ().
Description of the parameter (v,l):
The V represents the array, L represents the list, such as Execve () and Execle () is the difference between specifying the arguments, EXECVE is the passed array, and the execle is passing a list of strings.
Description of the environment variable (e,-):
The representation with subtitle E can pass an environment variable to a new program with a single argument, otherwise you can only inherit the caller's environment variable.
2. The interpreter script actually utilizes the EXEC () series function to implement the execution of the text Format command (the machine can actually only execute binary executables, and for the Text Format command program is actually called EXEC () through an interpreter, such as the shell, and awk,sed, Perl,python,ruby. )。 Note also that for this program, the first line of text needs to indicate the path name of the interpreter used (in "#. "Beginning.)
3. The file descriptor opened by Exec's calling program remains open during exec execution and still works in the new program, which is a double-edged sword. First of all, the benefits, such as executing a command under the shell, the child shell directly output the output to the standard output when executing a new command without having to reopen the standard output (of course, the command here is an internal command, which he does not need to fork for the internal command of the shell). The downside, then, is security, and it's a good idea to close those unnecessary file descriptors before executing the new program. However, the direct shutdown does not meet the recovery when exec fails, because opening the same file again does not guarantee a consistent file descriptor. This is the time to use the "Turn off flag on execution" and use the FCNTL () system to set the FD_CLOEXEX flag bit.
4. Since the execution of the EXEC series function will replace the text segment of the process, the signal processing function set before calling exec is of course overwritten. For signals that have previously been set to signal processing functions, the kernel resets their signal processing functions to SIG_DFL after the exec is performed. Signal SIGCHLD is a special case, the processing of the SIGCHLD signal is not specified in the SUSv3 before calling exec if the processing function is sig_ign. Therefore, for signal processing, the most common approach is to explicitly set to SIG_DFL before calling exec.
5. During the EXEC series function invocation, the process's signal mask and suspend signal settings are saved. This is also important, and it is best to remember this feature when you use it to avoid confusion.