1 进程的基本概念
UNIX标准把进程定义为一个其中运行着一个或多个线程的地址空间和这些线程所需要的系统资源。进程可以看作是正在运行着的程序,每个运行着的程序实例就构成一个进程。Linux系统会在进程之间共享程序代码和系统函数库,所以任何时刻内存中只有代码的一份副本。程序代码以只读的形式加载到内存中,虽不能对这个区域进行写操作,但可以被多个进程安全共享。系统动态库函数也可以共享,这可以节约大量磁盘和内存空间。但进程使用的变量是不同的,而且有自己的栈空间和环境变量,还有自己的程序计数器。
此外利用/proc
中的特殊文件我们可以窥视进程的内部情况。
Linux的虚拟内存系统能够把程序代码和数据以内存页面的形式放到硬盘中,所以Linux可以管理的进程比物理内存所能容纳的多得多。
每个进程都有一个唯一的进程标识符PID,数字1为特殊进程init保留。每个进程都由父进程启动,被其启动的叫子进程。在系统启动时,将运行一个init进程,该进程是系统运行的第一个进程。系统中的进程要么由init启动,要么由被init启动的进程启动。
ps -a
:输出系统上所有进程
ps -fl
:查看进程完整信息
ps -x
:查看STAT,表明进程的当前状态
2 进程调度
在单核的计算机上,同一时间只有一个进程可以运行,每个进程轮到的时间(时间片)很短,让用户觉得有多个进程在同时运行。Linux内核用进程调度器来决定下一个时间片应被分配给哪个进程,它是依据进程优先级的。Linux内核采用抢占式多任务处理,进程的挂起和继续运行无需彼此间的协作。系统根据进程的nice
值决定它的优先级,默认为0。可用renice
命令调整该值。
3 启动新进程
3.1 用system启动
我们可以在一个程序的内部启动另一个程序,从而创建新进程。
#include <stdlib.h>
int system(const char* string);
system函数运行以字符串参数形式传递的命令并等待该命令的完成,它必须用一个shell来启动需要的程序,所以在启动程序之前需要先启动一个shell,如果无法启动shell,将返回错误代码127,其他错误返回-1,否则返回该命令的退出码。
#include <stdlib.h>
#include <stdio.h>
int main()
{
printf("Running ps with system\n");
system("ps -l");
printf("Done\n");
exit(0);
}
3.2 用exec替换进程映像
exec系列函数把当前进程替换为一个新进程,新的程序启动后原来的程序就不再运行了,运行着的程序开始执行exec调用中指定的新的可执行文件中的代码。exec启动的进程其参数表和环境的总长度有限制,由ARG_MAX给出。一般exec不会返回,出现错误时返回-1并设置errno。exec启动的新进程继承了原来的文件描述符。
#include <unistd.h>
char** environ;
int execl(const char* path,const char* arg0,...,(char*)0);
int execlp(const char* file,const char* arg0,...,(char*)0);
int execle(const char* path,const char* arg0,...,(char*)0,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[]);
execl、execlp和execle参数可变,参数以空指针结尾。execv、execvp和execve参数是字符串数组,程序启动时这些参数被传递给被启动进程的main函数。
execlp和execvp通过搜索PATH环境变量来查找被启动程序的可执行文件路径,不在PATH中时要传递绝对路径。
execle和execve通过envp传递字符串数组来作为被启动进程的新环境变量。
全局变量environ可把一个值传递到新的程序环境中。
#include <unistd.h>
char* const ps_argv[] = {"ps","ax",0};
char* const ps_envp[] = {"PATH=/bin:/usr/bin","TERM=console",0};
printf("1: execl\n");
execl("/bin/ps","ps","-l",0);
printf("2: execlp\n");
execlp("ps","ps","-l",0);
printf("3: execle\n");
execle("/bin/ps","ps","-l",0,ps_envp);
printf("4: execv\n");
execl("/bin/ps",ps_argv);
printf("5: execvp\n");
execvp("ps",ps_argv);
printf("5: execve\n");
execve("/bin/ps",ps_argv,ps_envp);
3.3 用fork复制进程映像
如果想让多个函数同时执行,我们可以在进程中使用线程或从原程序中创建一个完全分离的进程,原来的进程和新创建的进程同时继续从fork调用向下执行。
#include <sys/types.h>
#include <unistd.h>
pid_t fork();
fork创建一个新进程,它复制当前进程并在进程表中创建一个新表项(Linux进程表像是一个数据结构,把当前加载到内存中的所有进程的相关信息保存在一个表中,PID是进程表的索引)。新进程与原进程几乎一样,执行代码一样但有自己的数据空间、环境和文件描述符。
在父进程中fork调用返回子进程的PID,子进程中fork调用返回0,fork失败返回-1。
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main()
{
char* message;
int n = 0;
printf("fork new process\n");
pid_t pid = fork();
switch(pid)
{
case -1:
perror("fork fail");
exit(1);
case 0:
message = "This is a child";
n = 5;
break;
default:
message = "This is a parent";
n = 3;
break;
}
for(int i = 0;i < n;i++)
{
puts(message);
sleep(1);
}
exit(0);
}
由于父进程在子进程结束之前结束,所以输出混乱。
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int* stat_loc);
在父进程中调用wait函数可以让父进程等待子进程的结束,它将暂停父进程直到它的子进程结束为止。这个调用返回子进程的PID,通常为已经结束运行的子进程PID。如果stat_loc不为空,子进程的退出状态码会被写入。
下面一些宏可以解释状态信息:
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main()
{
char* message;
int n = 0;
int exit_code;
printf("fork new process\n");
pid_t pid = fork();
switch(pid)
{
case -1:
perror("fork fail");
exit(1);
case 0:
message = "This is a child";
n = 5;
exit_code = 37;
break;
default:
message = "This is a parent";
n = 3;
exit_code = 0;
break;
}
for(int i = 0;i < n;i++)
{
puts(message);
sleep(1);
}
if(pid != 0)
{
int stat_val;
pid_t child_pid = wait(&stat_val);
printf("Child has finished:PID = %d\n",child_pid);
if(WIFEXITED(stat_val))
printf("Child exited with code %d\n",WEXITSTATUS(stat_val));
else
printf("Child terminated abnormally\n");
}
exit(exit_code);
}
wait总结:当父进程比子进程跑的块时,父进程在wait处阻塞等待子进程结束,获取子进程退出状态信息,而后向下继续运行。当子进程比父进程跑的快时,父进程在wait处不阻塞,直接获取子进程退出状态信息,而后向下继续运行。
4 僵尸进程
当子进程终止时,它与父进程之间的关联还会保持,直到父进程正常终止或父进程调用wait才结束。所以此时进程表中的子进程表项并不被释放,因为它的退出码需要保存,以备父进程在wait调用时使用,此时子进程成为一个死进程或僵尸进程。
如果子进程未结束而父进程异常终止,则子进程自动把init进程作为自己的父进程。子进程现在是一个不再运行的僵尸进程,被init进程接管。僵尸进程将一直保留在进程表中直到被init发现并释放。
5 等待指定的子进程
#include <sys/types.h>
#include <sys/wait.h>
pid_t waitpid(pid_t pid,int* stat_loc,int options);
waitpid用来等待指定的子进程。pid参数指定需要等待的子进程的PID,若值为-1则返回任一子进程的信息,options参数用来改变waitpid的行为。WNOHANG用来防止waitpid调用将调用者的执行挂起,可以用这个选项来查找是否有子进程已经结束,如果没有则程序将继续执行。