Botnets
In the previous article, we have understood the concepts of parent and child processes, and have mastered the usage of System Call exit, but few people may realize that, after a process calls exit, it does not disappear immediately, but leaves a data structure called Zombie.
Among the five States of a Linux Process, a zombie process is a very special one. It has abandoned almost all the memory space, no executable code, and cannot be scheduled, only one location is retained in the process list, and information such as the exit status of the process is recorded for collection by other processes. In addition, zombie processes no longer occupy any memory space. From this point of view, although the zombie process has a cool name, its influence is far from reaching those real botnets. Real botnets can always be scary, however, the zombie process does not have any effect on the system except for the information left by others.
Maybe readers are still curious about this new concept. Let's take a look at what the zombie process looks like in Linux.
When a process has exited, but its parent process has not called the system to call wait (which will be introduced later), it will remain zombie for the time before it is collected, let's write a simple Applet:
/* Zombie. C */
# Include <sys/types. h>
# Include <sys/Wait. H>
# Include <unistd. h>
# Include <stdlib. h>
# Include <stdio. h>
# Include <errno. h>
Main ()
{
Pid_t PID;
PID = fork ();
If (PID <0)/* if an error occurs */
Printf ("error occurred! N ");
Else if (pid = 0)/* if it is a sub-process */
Exit (0 );
Else/* if it is a parent process */
Sleep (60);/* sleep for 60 seconds. During this period, the parent process cannot do anything */
Wait (null);/* collect zombie processes */
}
Sleep is used to sleep a process for a specified number of seconds. Within these 60 seconds, the child process has exited, and the parent process is busy sleeping and cannot be collected, we can keep the sub-process in zombie status for 60 seconds.
Compile this program:
$ CC zombie. C-o zombie
Run the program in the background so that we can execute the following command:
$./Zombie &
[1] 1577
List the processes in the system:
$ PS-ax
......
1177 pts/0 s-Bash
1577 pts/0 s./zombie
1578 pts/0 Z [Zombie]
1579 pts/0 r PS-ax
Have you seen the "Z" in the middle? That is the sign of the botnet process. It indicates that process 1578 is now a zombie process.
We have learned that the system calls exit. Its function is to exit a process, but it is only limited to converting a normal process into a zombie process and cannot be completely destroyed. Although the zombie process has almost no impact on other processes, it does not take up CPU time, and the memory consumption is almost negligible. However, it still makes people feel uncomfortable when they stay there. In addition, the number of processes in Linux is limited. In some special cases, too many zombie processes may affect the generation of new processes. So how can we eliminate these zombie processes?
First, let's take a look at the reason for the zombie process. We know that Linux and UNIX are always rooted in different ways. The concept of a zombie process is also inherited from UNIX, the UNIX pioneers did not design this thing because they were bored and bored with other programmers. A zombie process stores a lot of information that is very important to programmers and system administrators. First, how does this process die? Is it normal to exit, is there an error, or is it forced to exit by other processes? Secondly, what is the total system CPU time used by the process and the total user CPU time? Number of page errors and the number of signals received. This information is stored in the zombie process. If there is no zombie process, all the information related to the process will be invisible as soon as the process exits. At this time, programmers or system administrators need to use this information, then I had to be dumpfounded.
So how can we collect this information and end these zombie processes? It depends on the waitpid call and wait call we will talk about below. Both of them are used to collect information left by the zombie process and completely disappear the process. The two calls are described in detail below.
Wait
Introduction
The wait function is prototype:
# Include/* define the pid_t type */
# Include
Pid_t wait (int * Status)
Once a process calls wait, it immediately blocks itself. Wait automatically analyzes whether a sub-process of the current process has exited. If it finds such a sub-process that has become a zombie, wait will collect information about this sub-process and destroy it completely and return it. If such a sub-process is not found, wait will be blocked until one appears.
The status parameter is used to save some statuses when the collection process exits. It is a pointer to the int type. But if we don't care about how this sub-process died, we just want to destroy it (in fact, in most cases, we will think like this ), we can set this parameter to null, as shown below:
PID = wait (null );
If the call succeeds, wait returns the ID of the collected sub-process. If the call process does not have a sub-process, the call fails. In this case, wait returns-1 and errno is set to echild.
Practice
Next let's use an example to apply wait calls. Fork is called by the system in the program. If you are not familiar with this or have forgotten about it, refer to System Call related to process management in the previous article (1 ).
/* Wait1.c */
# Include <sys/types. h>
# Include <sys/Wait. H>
# Include <unistd. h>
# Include <stdlib. h>
# Include <stdio. h>
# Include <errno. h>
Main ()
{
Pid_t PC, PR;
PC = fork ();
If (Pc <0)/* if an error occurs */
Printf ("error ocurred! N ");
Else if (Pc = 0) {/* if it is a sub-process */
Printf ("this is child process with PID of % DN", getpid ());
Sleep (10);/* sleep for 10 seconds */
}
Else {/* if it is a parent process */
PR = wait (null);/* Wait here */
Printf ("I catched a child process with PID of % DN"), Pr );
}
Exit (0 );
}
Compile and run:
$ CC wait1.c-O Wait1
$./Wait1
This is child process with PID of 1508
I catched a child process with PID of 1508
We can obviously note that there is a waiting time of 10 seconds before the results of the first row are printed. This is the time we set for the sub-process to sleep. Only the sub-process wakes up from its sleep, it can exit normally and be captured by the parent process. In fact, no matter how long the sub-process sleeps, the parent process will keep waiting. If you are interested, you can try to modify the value by yourself, see what results will appear.
Parameter status
If the value of the status parameter is not null, wait will take out the status of the sub-process and store it in it. This is an integer (INT ), it indicates whether the sub-process Exits normally or is ended abnormally (a process can also be ended by another process with a signal, which will be introduced in future articles ), and the return value at the normal end, or information about which signal is terminated. Because the information is stored in different binary bits of an integer, It is very troublesome to read it using the conventional method. People have designed a special macro (macro) to complete this work, next, let's take a look at two of the most common ones:
● Wifexited (Status)
This macro indicates whether the sub-process Exits normally. If yes, it returns a non-zero value.
(Please note that, although the name is the same, the parameter status here is not the only parameter of wait -- the pointer to the integer status, but the integer pointed to by the pointer. Remember not to confuse it .)
● Wexitstatus (Status)
When wifexited returns a non-zero value, we can use this macro to extract the return value of the sub-process. If the sub-process calls exit (5) to exit, wexitstatus (Status) will return 5; if the sub-process calls exit (7), wexitstatus (Status) returns 7. Note that if the process does not exit normally, that is, if wifexited returns 0, this value is meaningless.
The following is an example of what we just learned:
/* Wait2.c */
# Include <sys/types. h>
# Include <sys/Wait. H>
# Include <unistd. h>
Main ()
{
Int status;
Pid_t PC, PR;
PC = fork ();
If (Pc <0)/* if an error occurs */
Printf ("error ocurred! N ");
Else if (Pc = 0) {/* sub-process */
Printf ("this is child process with PID of % d. N", getpid ());
Exit (3);/* the sub-process returns 3 */
}
Else {/* parent process */
PR = wait (& status );
If (wifexited (Status) {/* If wifexited returns a non-zero value */
Printf ("the child process % d exit normally. N", Pr );
Printf ("The return code is % d. N", wexitstatus (Status ));
} Else/* If wifexited returns zero */
Printf ("the child process % d exit abnormally. N", Pr );
}
}
Compile and run:
$ CC wait2.c-O wait2
$./Wait2
This is child process with PID of 1538.
The child process 1538 exit normally.
The return code is 3.
The parent process accurately captures the returned value 3 of the child process and prints it out.
Of course, there are more than two macros that process exit state, but most of them are rarely used in normal programming, so it is not a waste of space here, interested readers can refer to Linux man pages to learn about their usage.
Process Synchronization
Sometimes, the parent process requires the calculation result of the child process to perform the next operation, or the sub-process function is a prerequisite for the parent process to perform the next step (for example, the sub-process creates a file, the parent process writes data). At this time, the parent process must stop at a certain position and wait until the child process finishes running. If the parent process runs directly without waiting, you can imagine that, there will be great confusion. This situation is called synchronization between processes. More accurately, this is a special case of process synchronization. Process Synchronization is to coordinate more than two processes so that they can be executed in order. There are more common methods to solve the process synchronization problem. We will introduce it later, but we can simply use the wait system to call this situation. Please refer to the following procedure:
# Include <sys/types. h>
# Include <sys/Wait. H>
# Include <unistd. h>
Main ()
{
Pid_t PC, PR;
Int status;
PC = fork ();
If (Pc <0)
Printf ("error occured on forking. N ");
Else if (Pc = 0 ){
/* Sub-process work */
Exit (0 );
} Else {
/* Work of the parent process */
PR = wait (& status );
/* Use sub-process results */
}
}
This program is just an example and cannot be used for execution, but it illustrates some problems. First, when the fork call is successful, the Parent and Child processes perform various tasks separately, however, when the work of the parent process has come to an end and the sub-process results need to be used, it will stop and call wait until the sub-process stops running and then continue to execute with the sub-process results, in this way, we have successfully solved the problem of process synchronization.
Waitpid
Introduction
The prototype of waitpid system calling in the Linux function library is:
# Include/* define the pid_t type */
# Include
Pid_t waitpid (pid_t PID, int * status, int options)
In essence, the functions of the system call waitpid and wait are exactly the same, but the waitpid has two more user-controlled parameters PID and options, this provides us with a more flexible way of programming. The following two parameters are described in detail:
● PID
From the parameter name PID and type pid_t, we can see that what is needed here is a process ID. But when the PID gets different values, it has different meanings here.
When the PID is greater than 0, only the child process whose process ID is equal to the PID is waiting. No matter how many other child processes have ended and exited, as long as the specified child process has not ended, waitpid will keep waiting.
When pid =-1, wait for any sub-process to exit without any restrictions. At this time, waitpid and wait play the same role.
When pid = 0, wait for any sub-process in the same process group. If the sub-process has been added to another process group, waitpid will not ignore it.
PID <-1, wait for any sub-process in a specified process group, the ID of this process group is equal to the absolute value of PID.
● Options
Options provides some additional options to control waitpid. Currently, only the wnohang and wuntraced options are supported in Linux. These two constants can be connected using the "|" operator, for example:
Ret = waitpid (-1, null, wnohang | wuntraced );
If you do not want to use them, you can set options to 0, for example:
Ret = waitpid (-1, null, 0 );
If the wnohang parameter is used to call waitpid, even if no sub-process exits, it will return immediately and will not wait forever like wait.
However, the wuntraced parameter involves some tracking and debugging knowledge and is rarely used. There is not much to worry about here. Interested readers can check the relevant materials on their own.
As you can see, smart readers may already see the clues-isn't wait a packaged waitpid? Check the <kernel source code directory>/include/unistd. h file 349-352 to find the following program segments:
Static inline pid_t wait (int * wait_stat)
{
Return waitpid (-1, wait_stat, 0 );
}
Returned values and errors
The return value of waitpid is slightly more complex than that of wait. There are three cases:
● When the returned result is normal, waitpid returns the ID of the collected sub-process;
● If the wnohang option is set, and waitpid in the call finds that no child process has exited to collect data, 0 is returned;
● If an error occurs in the call,-1 is returned. errno is set to a value to indicate the error;
When the sub-process indicated by the PID does not exist or the process exists but is not a sub-process that calls the process, waitpid will return an error, and errno is set to echild;
/* Waitpid. C */
# Include
# Include
# Include
Main ()
{
Pid_t PC, PR;
PC = fork ();
If (Pc <0)/* If fork fails */
Printf ("error occured on forking. N ");
Else if (Pc = 0) {/* if it is a sub-process */
Sleep (10);/* sleep for 10 seconds */
Exit (0 );
}
/* If it is a parent process */
Do {
PR = waitpid (PC, null, wnohang);/* The wnohang parameter is used, and waitpid will not wait here */
If (Pr = 0) {/* If the sub-process is not collected */
Printf ("No child exitedn ");
Sleep (1 );
}
} While (Pr = 0);/* If the sub-process is not collected, go back and try again */
If (Pr = pc)
Printf ("successfully get child % DN", Pr );
Else
Printf ("some error occuredn ");
}
Compile and run:
$ CC waitpid. C-o waitpid
$./Waitpid
No child exited
No child exited
No child exited
No child exited
No child exited
No child exited
No child exited
No child exited
No child exited
No child exited
Successfully get child 1526
After 10 failed attempts, the parent process finally collected the exited child process.
Because this is just an example program, it is inconvenient to write too complicated, so we let the Parent and Child processes sleep for 10 seconds and 1 second, respectively, it means that they work for 10 seconds and 1 second respectively. Both parent and child processes have work to do. The parent process uses the short interval of work to check whether the child process exits. If the child process exits, it is collected.
Some readers may have read this series of articles from the very beginning, but there is still a big question: Since all new processes are generated by fork, in addition, the sub-processes generated by fork are almost identical with the parent process. Doesn't that mean that all the processes in the system should be identical? Moreover, in our common sense, when we execute a program, the content of the new process should be the content of the program. Did we get it wrong? Obviously not. To solve these questions, we must mention the exec system call that we will introduce below.
Introduction
It is called by the exec system. In fact, in Linux, there is no exec () function form. Exec refers to a group of functions, with a total of six functions:
# Include
Int execl (const char * path, const char * Arg ,...);
Int execlp (const char * file, const char * Arg ,...);
Int execle (const char * path, const char * Arg,..., char * const envp []);
Int execv (const char * path, char * const argv []);
Int execvp (const char * file, char * const argv []);
Int execve (const char * path, char * const argv [], char * const envp []);
Among them, only execve is a real system call, and others are packaged library functions on this basis.
The exec function family finds an executable file based on the specified file name and uses it to replace the content of the calling process. In other words, it executes an executable file within the calling process. The executable file can be either a binary file or any script file that can be executed in Linux.
Unlike in general, functions in the exec function family are not returned after successful execution, because the entity of the calling process, including the code segment, data segment, and stack, has been replaced by new content, only some superficial information such as the process id remains unchanged, which is quite similar to the "golden shell" in the "Plan ". It looks like an old shell, but it has injected a new soul. Only when the call fails will they return a-1, which will be executed from the original program's call point.
Now we should understand how a new program is executed in Linux. Every time a process thinks that it cannot make any contribution to the system and support, it can make full use of the last point, call any exec to regenerate itself with a new look; or, more commonly, if a process wants to execute another program, it can fork a new process, then call any exec, which looks like a new process is generated by executing the application.
In fact, the second scenario is so widely used that Linux has made special optimizations for it. We already know that, fork will copy all the content of the calling process to the newly generated child process, which consumes a lot of time. If fork is finished, we will call exec immediately, these hard-to-copy items will be immediately erased, which seems very uneconomical, so people have designed a technology called "Copy-on-write, so that the fork does not copy the content of the parent process immediately after the end, but is copied only when the fork is actually practical, so that if the next statement is exec, it will not be useless, this improves efficiency.
A little deeper
The above six functions seem very complex, but in fact they are very similar in both functions and usage, with only a small difference. Before learning about them, let's take a look at the common main function.
The form of the main function below may be somewhat unexpected:
Int main (INT argc, char * argv [], char * envp [])
It may be different from what is described in most textbooks, but in fact, this is the real complete form of the main function.
The argc parameter specifies the number of command line parameters when the program is run. The array argv stores all command line parameters, and the array envp stores all environment variables. Environment Variables refer to a set of values that have existed since user login. Many applications need to rely on them to determine system details. Our most common environment variables are path, it points out where to search for applications, such as/bin; home is also a common environment variable, it points out our personal directory in the system. Environment variables generally exist as strings "xxx = xxx". xxx indicates the variable name, and XXX indicates the variable value.
It is worth mentioning that the argv array and the envp array both store pointer to the string. Both arrays end with a null element.
We can use the following program to view what is uploaded to argc, argv, and envp:
/* Main. C */
Int main (INT argc, char * argv [], char * envp [])
{
Printf ("n ### argc ### n % DN", argc );
Printf ("n ### argv ### N ");
While (* argv)
Printf ("% Sn", * (argv ++ ));
Printf ("n ### envp ### N ");
While (* envp)
Printf ("% Sn", * (envp ++ ));
Return 0;
}
Compile it:
$ CC main. C-o main
When running, we intentionally add a few command line parameters that do not have any function:
$./Main-xx 000
### Argc ###
3
### Argv ###
./Main
-Xx
000
### Envp ###
Pwd =/home/lei
Remotehost = dt.laser.com
Hostname = localhost. localdomain
Qtdir =/usr/lib/qt-2.3.1
Lessopen = |/usr/bin/lesspipe. sh % s
Kdedir =/usr
User = lei
Ls_colors =
Machtype = i386-redhat-linux-gnu
Mail =/var/spool/mail/lei
Inputrc =/etc/inputrc
Lang = en_us
LOGNAME = lei
Shlvl = 1
Shell =/bin/bash
Hosttype = i386
Ostype = Linux-GNU
History Size = 1000
Term = ANSI
Home =/home/lei
Path =/usr/local/bin:/usr/x11r6/bin:/home/lei/bin
_ =./Main
We can see that the program uses "./main" as 1st command line parameters, so we have three command line parameters. This may be a little different from what you usually get used to. Be careful not to make a mistake.
Now let's look at the exec function family and focus on execve:
Int execve (const char * path, char * const argv [], char * const envp []);
Compare the complete form of the main function. Do you see the problem? Yes, the argv and envp in these two functions have a one-to-one relationship. The execve 1st parameter path is the complete path of the application to be executed, and the 2nd parameter argv is the command line parameter passed to the application to be executed, the 3rd envp parameter is the environment variable passed to the application to be executed.
Take a look at these six functions and you can find that the first three functions start with execl, and the last three functions start with execv. The difference between them is that, the functions starting with execv pass the command line parameters in the form of "char * argv []", while the functions starting with execl use a method that we prefer to list parameters one by one, end with a null expression. Here, the role of null is the same as that of null in the argv array.
Of all six functions, only execle and execve use char * envp [] to pass environment variables. None of the other four functions have this parameter, this does not mean that they do not pass environment variables. These four functions will pass the default environment variables to the executed applications without any modification. Execle and execve Replace the default ones with the specified environment variables.
There are two other functions execlp and execvp ending with P. It seems that they have very little difference with execl and execv, and that is true, too, all four functions except execlp and execvp require that their 1st parameter paths must be a complete path, such as "/bin/ls "; the 1st file parameters of execlp and execvp can be simply a file name, such as "ls". These two functions can be automatically searched in the directory specified by the Environment Variable path.
Practice
The knowledge is almost introduced. Next let's look at the actual application:
/* Exec. C */
# Include
Main ()
{
Char * envp [] = {"Path =/tmp ",
"User = lei ",
"Status = testing ",
Null };
Char * argv_execv [] = {"Echo", "excuted by execv", null };
Char * argv_execvp [] = {"Echo", "executed by execvp", null };
Char * argv_execve [] = {"env", null };
If (Fork () = 0)
If (execl ("/bin/echo", "Echo", "executed by execl", null) <0)
Perror ("Err on execl ");
If (Fork () = 0)
If (execlp ("Echo", "Echo", "executed by execlp", null) <0)
Perror ("Err on execlp ");
If (Fork () = 0)
If (execle ("/usr/bin/ENV", "env", null, envp) <0)
Perror ("Err on execle ");
If (Fork () = 0)
If (execv ("/bin/echo", argv_execv) <0)
Perror ("Err on execv ");
If (Fork () = 0)
If (execvp ("Echo", argv_execvp) <0)
Perror ("Err on execvp ");
If (Fork () = 0)
If (execve ("/usr/bin/ENV", argv_execve, envp) <0)
Perror ("Err on execve ");
}
The program calls two common Linux system commands, ECHO and Env. Echo will print out the command line parameters that follow, and env will be used to list all environment variables.
As the execution sequence of each sub-process cannot be controlled, a chaotic output may occur-the printed results of each sub-process are mixed together, rather than strictly following the order listed in the program.
Compile and run:
$ CC exec. C-o Exec
$./Exec
Executed by execl
Path =/tmp
User = lei
Status = testing
Executed by execlp
Excuted by execv
Executed by execvp
Path =/tmp
User = lei
Status = testing
As expected, the result of execle output goes to the front of execlp.
In normal programming, if you use the exec function family, you must add an incorrect judgment statement. Compared with other system calls, exec is prone to injury, and the execution file location, permissions, and many other factors can cause the call to fail. The most common errors are:
The file or path cannot be found, and errno is set to enoent;
Array argv and envp forget to end with null. errno is set to efault;
You are not authorized to run the file to be executed. errno is set to eacces.
Process life
Let me use some image metaphor to make a small summary of the process's short life:
With a fork statement, a new process is born, but it is only a clone of the old process.
Then, with exec, the new process was reborn and independent from home, and started a career serving the people.
A person is dead, and the process is the same. It can be a natural death, that is, the last "}" that runs to the main function. It can also be a suicide, there are two ways to commit suicide. One is to call the exit function, and the other is to use return in the main function. Either way, it can leave a suicide note, stored in the return value; it can even be murdered and ended by other processes in other ways.
After the process dies, a zombie will be left behind. Wait and waitpid act as the zombie and push the zombie to be credened, making it invisible.
This is the complete life of the process.
Summary
This article focuses on the wait, waitpid, and exec Function Families for system calls. The introduction to system calls related to process management is coming to an end here. In the next article, this is also the last article about system calls related to process management. We will review our recent knowledge through two cool practical examples.