文章目录
- 查看系统中的进程
- 进程标识
- 进程创建
- 进程终止
- 僵尸进程
- 守护进程和孤儿进程
- wait函数
- exec函数
- system函数
程序是存放在磁盘文件中的可执行文件。程序的执行实例被称为进程,进程具有独立的权限与职责。
每个进程运行在其各自的虚拟地址空间中,进程之间可以通过由内核控制的机制相互通讯。如果系统中某个进程崩溃,它不会影响到其余的进程。
每个Linux进程都一定有一个唯一的数字标识符,称为进程ID,PID(进程ID)总是一个非负整数。
在进程的main函数执行之前内核会启动,编译器在编译的时候会将启动例程编译进可执行文件中。
启动例程的作用:搜集命令行的参数传递给main函数中的argc和argv;搜集环境信息构建环境表并传递给main函数;登记进程的终止函数。
查看系统中的进程
通过下面的命令就可以查看当前系统执行的进程。
ps
ps -ef
ps -aux
ps -ef | more
当前系统执行的进程如下图所示。
USER是指进程的属主;PID:进程ID号;PPID:父进程ID号;%CPU:进程占用的CPU百分比;%MEM:占用内存的百分比;VSZ:进程虚拟大小;RSS:驻留中页的数量;TTY:终端ID;STAT:进程的状态;START:启动进程的时间;TIME:进程消耗CPU的时间;COMMAND:命令的名称和参数。
进程常见的状态:运行状态®、等待状态(S)、停止状态(T)、僵尸状态(Z)。僵尸状态指的是进程终止或结束,但是在进程表项中仍有记录。
进程标识
获取进程相关标识的函数原型如下。
#include <unistd.h>
#include <sys/types.h>
pid_t getpid(void); //获取当前进程ID
uid_t getuid(void); //获得当前进程的实际用户ID
uid_t geteuid(void); //获得当前进程的有效用户ID
gid_t getgid(void); //获得当前进程的用户组ID
pid_t getppid(void); //获得当前进程的父进程ID
pid_t getpgrp(void); //获得当前进程所在的进程组ID
pid_t getpgid(pid_t pid); //获得进程ID为pid的进程所在的进程组ID
关于进程标识相关的代码示例如下。
#include <stdio.h>
#include <unistd.h>
int main()
{
printf("pid : %d\n",getpid());
printf("ppid : %d\n",getppid());
printf("uid : %d\n",getuid());
printf("euid : %d\n",geteuid());
printf("user gid : %d\n",getgid());
printf("gid : %d\n",getpgrp());
printf("pgid : %d\n",getpgid(getpid()));
printf("ppgid : %d\n",getpgid(getppid()));
return 0;
}
程序编译之后的执行结果如下图所示。
进程创建
进程创建使用的头文件及函数原型如下。
#include <unistd.h>
#include <sys/types.h>
pid_t fork(void);
pid_t vfork(void);
进程创建函数调用第一,返回两次,在子进程中返回值为0,在父进程中返回值是子进程的进程ID号。
使用进程创建函数fork()创建子进程,父子进程哪个先运行根据系统调度决定,子进程会复制(继承)父进程的内存空间。
使用进程创建函数vfork()创建子进程,子进程先运行且不复制父进程的内存空间。
子进程继承父进程的属性包括:用户信息和权限、目录信息、信号信息、环境、共享存储段、资源限制、堆、栈和数据段,代码段是父子进程共享的。
子进程的特有属性:进程ID、锁信息、运行时间、未决信号。
操作文件时的内核结构变化:子进程只继承父进程的文件描述表,不继承但共享文件表项和i-node;父进程创建一个子进程后,文件表项中的引用计数器加1变成2,当父进程作close操作后,计数器减1,子进程还是可以使用文件表项,只有当计数器为0时才会释放文件表项。
使用fork函数创建进程的代码示例如下。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
pid_t pid;
printf("当前运行进程的ID : %d\n",getpid());
pid = fork();
if(pid < 0)
perror("Error!\n");
else if(pid == 0)
printf("子进程在运行!fork返回值:%d,当前进程ID:%d,父进程ID:%d\n",pid,getpid(),getppid());
else
printf("父进程在运行!fork返回值:%d,当前进程ID:%d,父进程ID:%d\n",pid,getpid(),getppid());
printf("当前运行进程的ID : %d\n",getpid());
sleep(1);
return 0;
}
上面程序编译后运行结果如下图所示。
可以清楚的看到,在进程创建完毕后,系统中就有了两个进程,一句打印输出的代码被父子进程各执行了一次!
父进程和通过fork()函数创建出的子进程有相同的虚拟空间,但是却有各自的物理空间!
下面的例子就能够说明这一点。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int global_v = 10;
int main()
{
int area_v = 10;
static int static_v = 10;
pid_t pid;
printf("当前运行进程的ID : %d\n",getpid());
pid = fork();
if(pid < 0)
perror("Error!\n");
else if(pid == 0)
{
global_v = 20;
area_v = 20;
static_v = 20;
printf("子进程中,global_v地址:%p area_v地址:%p static_v地址:%p\n",&global_v,&area_v,&static_v);
printf("子进程在运行!fork返回值:%d,当前进程ID:%d,父进程ID:%d\n",pid,getpid(),getppid());
}
else
{
global_v = 30;
area_v = 30;
static_v = 30;
printf("父进程中,global_v地址:%p area_v地址:%p static_v地址:%p\n",&global_v,&area_v,&static_v);
printf("父进程在运行!fork返回值:%d,当前进程ID:%d,父进程ID:%d\n",pid,getpid(),getppid());
}
printf("当前运行进程的ID : %d, global_v = %d, area_v = %d, static_v = %d\n",getpid(),global_v,area_v,static_v);
return 0;
}
程序编译后的运行结果如下图所示。
可以看到,在父子进程中打印的变量地址是一样的(这里打印的变量地址是虚拟地址,并非物理地址),但是里面存放的值却是不一样的,这说明父子进程有相同的虚拟空间,却各自映射了独立的物理空间。
关于虚拟空间和物理空间,下面是一个例子。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
int global_v = 10;
int main()
{
int area_v = 10;
static int static_v = 10;
pid_t pid;
FILE *fp = fopen("a.txt","w"); //带缓存
int fd = open("b.txt",O_WRONLY|O_CREAT|O_TRUNC,S_IRWXU|S_IRWXG); //不带缓存
char *s = "helloworld!";
ssize_t size = strlen(s)*sizeof(char);
fprintf(fp,"字符串s : %s pid : %d\n",s,getpid()); //标准IO函数,带缓存,先写入缓存,再写入文件
write(fd,s,size); //内核提供的IO系统调用,不带缓存,直接写入文件
pid = fork();
if(pid < 0)
perror("Error!\n");
else if(pid == 0)
{
global_v = 20;
area_v = 20;
static_v = 20;
printf("子进程在运行!fork返回值:%d,当前进程ID:%d,父进程ID:%d\n",pid,getpid(),getppid());
}
else
{
global_v = 30;
area_v = 30;
static_v = 30;
printf("父进程在运行!fork返回值:%d,当前进程ID:%d,父进程ID:%d\n",pid,getpid(),getppid());
}
fprintf(fp,"当前运行进程的ID : %d, global_v = %d, area_v = %d, static_v = %d\n",getpid(),global_v,area_v,static_v);
//写入父子进程各自的缓存中,然后清缓存并写入文件中
fclose(fp);
close(fd);
return 0;
}
程序编译后的执行结果如下图所示。
fork()生成的子进程会将父进程中的缓存复制一份,就像上面图中划线的两行,内容是完全相同的,此后,父子进程再写入缓存中各自的部分,最后一起写入一个文件中。系统调用是没有缓存的,内容是直接写入文件的。
子进程会继承父进程的文件描述符,下面是父进程将文件偏移量移动到文件尾,再由子进程来追加信息的例子。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
#include <sys/types.h>
int main(int argc,char *argv[])
{
if(argc < 2)
{
fprintf(stderr,"usage : %s filename\n",argv[0]);
exit(1);
}
int fd = open(argv[1],O_WRONLY|O_CREAT|O_TRUNC,S_IRWXU|S_IRWXG);
if(fd < 2)
{
perror("open error");
exit(1);
}
pid_t pid = fork();
if(pid < 0)
perror("fork error");
else if(pid > 0)
{
char *s = "Parent process input!\n";
ssize_t size = strlen(s)*sizeof(char);
if(write(fd,s,size) != size)
{
perror("write error");
exit(1);
}
else
printf("Parent process write success!\n");
if(lseek(fd,0,SEEK_END)<0) //将文件偏移量移动到文件尾
{
perror("lseek error");
exit(1);
}
else
printf("Parent process lseek success!\n");
}
else
{
char *s = "Child process input!\n";
ssize_t size = strlen(s)*sizeof(char);
sleep(1); //等待父进程调节偏移量
if(write(fd,s,size) != size) //fd是从父进程中复制来的,指向同一个文件
{
perror("write error");
exit(1);
}
else
printf("Child process write success!\n");
}
printf("current pid : %d\n",getpid());
sleep(1); //等待子进程写入完毕
close(fd);
return 0;
}
上面程序编译后的执行结果如下图所示。
通过上面程序的运行结果可以看到,子进程在父进程写入的文件中追加了部分信息。
在程序中使用多个fork()函数的代码示例如下。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
pid_t pid = fork();
if(pid < 0)
perror("fork error");
else
{
pid = fork();
if(pid < 0)
perror("fork error");
else
{
pid = fork();
if(pid < 0)
perror("fork error");
}
}
printf("current pid : %d\n",getpid());
sleep(1);
return 0;
}
产生的进程数是以2为底数增长的,代码中使用了几个fork()函数,产生的进程数就是2的几次方。
进程链和进程扇
进程链是指父进程创建子进程,子进程再创建子进程这样的链式结构,每个进程只有向下的一个分支;进程扇指的是一个父进程创建多个子进程,只有父进程有多个分支。
进程链的代码示例如下。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(int argc,char* argv[])
{
int num = 0;
if(argc < 2)
{
fprintf(stderr,"usage : %s number\n",argv[0]);
exit(1);
}
else
{
num = atoi(argv[1]);
if(num < 2)
num = 2;
}
pid_t pid;
for(int i=1;i<num;i++)
{
pid = fork();
if(pid < 0)
{
perror("fork error");
exit(1);
}
else if(pid > 0)
break; //父进程退出循环
//子进程就接着执行循环
}
printf("pid : %d ppid : %d \n",getpid(),getppid());
sleep(1); //等待所有进程打印完毕
return 0;
}
上面程序编译后的执行结果如下图所示。
关于进程扇的执行结果如下,代码只需要在进程链代码的基础上将父进程退出循环改为子进程退出循环即可。
进程终止
进程终止分为正常终止、异常终止和进程返回。
正常终止:从main函数返回;调用exit(标准C库函数);调用_exit或_Exit(系统调用);最后一个线程从其启动例程返回;最后一个线程调用pthread_exit。
异常终止:调用abort;接收到一个信号并终止;最后一个线程对取消请求做处理响应。
进程返回:通常程序运行成功返回0,否则返回非0;在shell中可以查看进程的返回值。
每个启动的进程都默认登记了一个标准的终止函数,终止函数在进程终止时释放进程所占用的一些资源,登记的多个终止函数执行顺序是以栈的方式执行,先登记的后执行。
#include <stdlib.h>
int atexit(void (*function(void))); //向内核登记终止函数,成功返回0,出错返回-1
进程终止的简单代码示例如下。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
//定义进程的终止函数
void term_fun1()
{
printf("This is the first terminal function!\n");
}
void term_fun2()
{
printf("This is the second terminal function!\n");
}
void term_fun3()
{
printf("This is the third terminal function!\n");
}
int main(int argc,char *argv[])
{
if(argc < 3)
{
fprintf(stderr,"usage: %s filename exit or _exit or return\n",argv[0]);
exit(1);
}
//向内核登记终止函数
atexit(term_fun1);
atexit(term_fun2);
atexit(term_fun3);
FILE *fp = fopen(argv[1],"w");
fprintf(fp,"hello world!\n"); //写入文件
if(!strcmp(argv[2],"exit"))
exit(0); //标准C库函数
else if(!strcmp(argv[2],"_exit"))
_exit(0); //系统调用
else if(!strcmp(argv[2],"return"))
return 0;
else
fprintf(stderr,"usage: %s filename exit or _exit or return\n",argv[0]);
exit(0);
}
程序编译后的执行结果如下图所示。
可以看到,终止函数确实是按照栈的方式执行的,先登记的后执行,后登记的先执行。
通过上面的执行结果也可以发现return、exit和系统调用_exit/_Exit的区别。
return | exit() | _exit() / _Exit() | |
---|---|---|---|
是否刷新标准I/O缓存 | 是 | 是 | 否 |
是否自动调用终止函数 | 是 | 是 | 否 |
僵尸进程
子进程结束但是没有完全释放内存,该进程就称为僵尸进程(zombie)。僵尸进程父进程结束后,该僵尸进程就会被init进程领养,最终被系统回收。
避免僵尸进程的方法:让僵尸进程的父进程来回收,父进程每隔一段时间来查询子进程是否结束并回收,调用wait()或者waitpid(),通知内核释放僵尸进程;采用信号SIGCHLD通知处理,并在信号处理程序中调用wait函数;让僵尸进程成为孤儿进程,由init进程回收。
关于僵尸进程的使用例子如下。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
pid_t pid = fork();
if(pid < 0)
perror("fork error");
else if(pid == 0)
{
printf("pid : %d ppid : %d\n",getpid(),getppid());
return 0; //子进程结束成为僵尸进程
//使用exit(0)结束也可以
}
while(1)
{
sleep(5);
}
return 0;
}
程序运行后再开一个终端,使用命令查看当前的进程,可以看到,子进程退出后由于没有释放内存,变为了僵尸进程。
也可以让程序在后台运行,只需要在命令的结尾加上&符号即可,然后使用kill -9命令杀死僵尸进程的父进程,再使用ps命令查看当前的进程信息,僵尸进程就不存在了。
守护进程和孤儿进程
守护进程(daemon)是生存期长的一种进程,它们常常在系统引导装入时启动,在系统关闭时终止。所有守护进程都以超级用户(用户ID为0)的优先权运行,守护进程没有控制终端,其父进程都是init进程。
父进程结束,子进程就成为孤儿进程(orphan),会由init进程领养。
关于孤儿进程的代码示例如下。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
pid_t pid = fork();
if(pid < 0)
perror("fork error");
else if(pid > 0)
{
sleep(1); //父进程先不退出,让子进程打印此时的父进程号
printf("父进程退出,pid : %d\n",getpid());
exit(0);
}
else
{
int i = 0;
while(1)
{
printf("子进程pid : %d 父进程pid : %d\n",getpid(),getppid());
i++;
if(i==2)
break;
sleep(2); //确保父进程退出
}
}
return 0;
}
上面程序编译后运行结果如下图所示。
可以看到,在父进程还没退出时,子进程打印的父进程号就是创建自己的进程号,在父进程退出后,子进程变为孤儿进程被init进程领养,父进程号也因此变为1。
wait函数
wait函数的原型及其要包含的头文件如下。
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status); //status为空时,代表任意状态结束的子进程,不为空则代表指定状态结束的子进程
pid_t waitpid(pid_t pid,int *status,int options);
//pid是-1,等待任一子进程,功能与wait等效;pid大于0,等待其进程ID与pid相等的子进程;pid等于0,等待其组ID等于调用进程的组ID的任一子进程;pid小于-1,等待其组ID等于pid的绝对值的任一子进程
//成功返回子进程的ID,出错返回-1
检查wait和waitpid函数返回终止状态的宏:
WIFEXITED / WEXITSTATUS(status) 若为正常终止子进程返回的状态,则为真。
WIFSIGNALED / WTERMSIG(status) 若为异常终止子进程返回的状态,则为真(接到一个不能捕捉的信号)。
WIFSTOPPED / WSTOPSIG(status) 若为当前暂停子进程的返回的状态,则为真。
options参数:
WNOHANG 若由pid指定的子进程没有退出则立即返回,则waitpid不阻塞,此时其返回值为0。
WUNTRACED 若某实现支持作业控制,则由pid指定的任一子进程状态已暂停,且其状态自暂停以来还未报告过,则返回其状态。
wait()函数的功能是等待子进程退出并回收,防止僵尸进程的产生;waitpid()函数是wait()函数的非阻塞版本。
wait()函数和waitpid()函数的区别:
在一个子进程终止前,wait使其调用者阻塞;waitpid有一个选择项options,可使调用者不阻塞;waitpid等待一个指定的子进程pid,返回的是等待子进程的状态,而wait等待所有的子进程,其返回任一终止子进程的状态。
使用kill -l命令查看所有信号的代表的意思。
关于wait函数的使用,其代码示例如下。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
void output_status(int status)
{
if(WIFEXITED(status))
printf("Normal exit : %d\n",WEXITSTATUS(status));
else if(WIFSIGNALED(status))
printf("Abnormal exit : %d\n",WTERMSIG(status));
else if(WIFSTOPPED(status))
printf("Stopped signal : %d\n",WSTOPSIG(status));
else
printf("Unknow signal!\n");
}
int main()
{
int status;
pid_t pid;
if((pid = fork()) < 0)
{
perror("fork error");
exit(1);
}
else if(pid == 0)
{
printf("pid : %d ppid : %d\n",getpid(),getppid());
exit(6); //子进程正常终止
}
wait(&status); //父进程调用wait函数阻塞,等待子进程结束并回收
output_status(status); //输出状态信息
if((pid = fork()) < 0)
{
perror("fork error");
exit(1);
}
else if(pid == 0)
{
printf("pid : %d ppid : %d\n",getpid(),getppid());
int i=1,j=0;
int k = i/j; //除数为0,异常终止
}
wait(&status); //父进程调用wait函数阻塞,等待子进程结束并回收
output_status(status); //输出状态信息
if((pid = fork()) < 0)
{
perror("fork error");
exit(1);
}
else if(pid == 0)
{
printf("pid : %d ppid : %d\n",getpid(),getppid());
pause(); //阻塞等待
//while(1) sleep(3); //和上面的pause语句效果一样
}
//wait(&status); //使用wait时,用kill发命令是异常退出,不是停止信号
do{
pid = waitpid(pid,&status,WNOHANG | WUNTRACED); //使用waitpid函数
if(pid == 0)
sleep(1);
}while(pid == 0);
output_status(status); //输出状态信息
return 0;
}
上面程序编译后的执行结果如下图所示。
在另一个终端里给当前阻塞的进程发送停止信号,通过返回的status就能打印出相应的错误信息。
一般在有父子进程的程序中,在父进程中加入wait(0),让其等待子进程结束后回收资源。
exec函数
在用fork函数创建子进程后,子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程完全由新程序代换,替换原有进程的正文,而新程序则从其main函数开始执行,因为调用exec并不创建新进程,所以前后的进程ID并未改变。exec只是用另一个新程序替换了当前进程的正文、数据、堆和栈段。
exec函数一般在子进程中进行调用。
exec函数原型如下。
#include <unistd.h>
int execl(const char* pathname,const char* arg0, .../*(char*) 0*/);
int execv(const char* pathname,char* const argv[]);
int execle(const char* pathname,const char* arg0,.../*(char*) 0,char *const envp[]*/);
int execve(const char* pathname,char* const argv[],char* const envp[]);
int execlp(const char* pathname,const char* arg0,.../*(char*) 0*/);
int execvp(const char* pathname,char* const argv[]);
//出错返回-1,成功则不返回
以上函数中,除了execve函数是系统调用外,其他函数都是库函数,执行execve函数,后面的代码不执行。
execlp函数和execvp函数中的pathname,相对路径和绝对路径均可使用,其他四个函数中只能使用绝对路径,相对路径一定要在进程环境表对应的PATH中。函数中的argv参数为新程序执行main函数中传递的参数,最后一个元素为NULL,envp是进程的环境表。
带有字母“I”的函数,表明后面的参数列表是要传递给程序的参数列表,参数列表的第一个参数必须是要执行程序,最后一个参数必须是NULL。
带有字母“p”的函数,第一个参数可以是相对路径或程序名,如果无法立即找到要执行的程序,那么就在环境变量PATH指定的路径中搜索。其他函数的第一个参数则必须是绝对路径名。
带有字母“v”的函数,表明程序的参数列表通过一个字符串数组来传递,这个数组和最后传递给程序的main函数的字符串数组argv完全一样。第一个参数必须是程序名,最后一个参数也必须是NULL。
带有字母“e”的函数,用户可以自己设置程序接收一个设置环境变量的数组。
关于exec函数调用的简单代码示例如下。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
char *cmd1 = "cat";
char *cmd2 = "/bin/cat";
char *argv1 = "/etc/passwd";
char *argv2 = "/etc/group";
int main(int argc,char *argv[])
{
pid_t pid;
if((pid = fork()) < 0)
{
perror("fork error");
exit(1);
}
else if(pid == 0) //子进程运行
{
if(execl(cmd2,cmd1,argv1,argv2,NULL) < 0)
{
perror("execl error");
exit(1);
}
else
{
printf("execl %s success!\n",cmd1);
}
}
wait(0); //wait(NULL); 父进程执行
return 0;
}
上面代码中,如果在execl()函数中使用相对路径,函数无法调用成功,提示文件不存在,第一个参数只能是绝对路径,execl()函数调用成功后,后面的代码也不再执行。
system函数
system函数的原型如下。
#include <stdlib.h>
int system(const char* command);
//成功返回执行命令的状态,出错返回-1
system函数的简单使用代码如下,打印系统时间。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
system("clear"); //清屏
system("date"); //当前时间
system("cal"); //本月日历
return 0;
}
程序编译后的执行结果如下图所示。
其功能相当于使用bash命令执行。
system函数的功能就是简化exec函数的使用,其内部构建了一个子进程,并由这个子进程调用exec函数。
关于自定义函数调用exec函数实现system函数的代码示例如下。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
char *cmd1 = "cal > s1.txt";
char *cmd2 = "cal > s2.txt";
void mysystem(char *cmd)
{
pid_t pid;
if((pid = fork()) < 0)
{
perror("fork error");
exit(1);
}
else if(pid == 0)
{
if(execlp("/bin/bash","/bin/bash","-c",cmd,NULL) < 0)
{
perror("execlp error");
exit(1);
}
}
wait(0);
}
int main()
{
system(cmd1); //使用系统函数
mysystem(cmd2); //使用自己定义的函数调用exec函数
return 0;
}
上面程序编译后的执行结果如下图所示。
参考视频:
Linux多进程编程详解