程序与进程
程序:是可执行文件,其本质是是一个文件,程序是静态的,同一个程序可以运行多次,产生多个进程
进程:它是程序的一次运行过程,当应用程序被加载到内存中运行之后它就称为了一个进程,进程是动态的,进程的生命周期是从程序运行到程序退出
父子进程:当一个进程A通过frok()函数创建出进程B,A为B的父进程,B为A的子进程
进程号(PID):也称进程标识符,是一个非负整数,用于唯一标识系统下某一个进程。
-
pid=0:交换进程,pid=1:init进程,
-
linux中可以使用ps -aux查看系统中的所有进程,可配合管道进行使用 ps -aux | grep xxx。
-
pid_t getpid(void):该系统调用用于获取当前进程的pid。
-
pid_t getppid(void):用于获取父进程的pid。
进程的创建
一个现有的进程可以调用 fork()函数创建一个新的进程, 调用 fork()函数的进程称为父进程,由 fork()函
数创建出来的进程被称为子进程(child process) , fork()函数原型如下所示(fork()为系统调用)。
应用场景:
- 在诸多的应用中,创建多个进程是任务分解时行之有效的方法,譬如,某一网络服务器进程可在监听客户端请求的同时,为处理每一个请求事件而创建一个新的子进程,与此同时,服务器进程会继续监听更多的客户端连接请求。
- 一个进程要执行不同的程序。 譬如在程序 app1 中调用 fork()函数创建了子进程,此时子进程是要
去执行另一个程序 app2,也就是子进程需要执行的代码是 app2 程序对应的代码,子进程将从 app2
程序的 main 函数开始运行。这种情况,通常在子进程从 fork()函数返回之后立即调用 exec 族函数
来实现,关于 exec 函数将在后面内容向大家介绍。
fork():
作用:用于创建一个子进程,原型如下:
c
#include <unistd.h>
pid_t fork(void);
理解 fork()系统调用的关键在于,完成对其调用后将存在两个进程,一个是原进程(父进程)、另一个
则是创建出来的子进程,并且每个进程都会从 fork()函数的返回处继续执行,会导致调用 fork()返回两次值,
子进程返回一个值、父进程返回一个值 。
c
#include <stdio.h>
#include <unistd.h>
int main()
{
pid_t pid;
pid = getpid();
fork(); //创建一个子进程,子进程从这开始执行
printf("pid = %d\r\n",pid); //父子进程都会执行该语句
return 0;
}
fork()调用成功后,会在父进程中返回子进程的 PID,而在子进程中返回值是 0;如果调用失败,父进
程返回值-1,不创建子进程,并设置 errno。
fork返回值总结:父子进程都会从fork()函数的返回处继续执行。
- 父进程:返回子进程PID,调用失败返回-1
- 子进程:返回0
c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
pid_t pid;
pid = fork();
switch(pid)
{
case -1:
perror("fork error");
exit(-1);
case 0:
printf("这是子进程打印的信息:父进程pid = %d,子进程pid = %d\r\n",getppid(),getpid());
_exit(0); //子进程使用_exit退出
default:
printf("这是父进程打印的信息:父进程pid = %d,子进程pid = %d\r\n",getpid(),pid);
exit(0);
}
}
vfork()
也是用于创建子进程,返回值与fork()一样。原型如下:
c
#include <sys/types.h>
#include <unistd.h>
pid_t vfork(void)
vfork与fork区别:
- vfork直接使用父进程存储空间,不拷贝。
- vfork()与 fork()一样都创建了子进程,但 vfork()函数并不会将父进程的地址空间完全复制到子进程中,因为子进程会立即调用 exec(_exit) ,于是也就不会引用该地址空间的数据。不过在子进程调用 exec 或_exit 之前,它在父进程的空间中运行、 子进程共享父进程的内存。这种优化工作方式的实现提高的效率; 但如果子进程修改了父进程的数据(除了 vfork 返回值的变量)、进行了函数调用、或者没有调用 exec 或_exit 就返回将可能带来未知的结果
- vfork保证子进程先运行,当子进程调用exit退出后,父进程才执行。
- vfork()保证子进程先运行, 子进程调用 exec 之后父进程才可能被调度运行。
虽然 vfork()系统调用在效率上要优于 fork(),但是 vfork()可能会导致一些难以察觉的程序 bug,所以尽量避免使用 vfork()来创建子进程,虽然 fork()在效率上并没有 vfork()高,但是现代的 Linux 系统内核已经采用了写时复制技术来实现 fork(),其效率较之于早期的 fork()实现要高出许多,除非速度绝对重要的场合,
- vfork()保证子进程先运行, 子进程调用 exec 之后父进程才可能被调度运行。
父子数据共享
使用fork()创建子进程后,子进程会把父进程中的数据拷贝一份;该实验中,子进程对a进行了+10操作,但是不影响父进程中的a。
c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
pid_t pid;
int a = 10;
pid = fork(); //fork创建子进程
switch(pid)
{
case -1:
perror("fork error");
exit(-1);
case 0:
a+=10; //子进程中改变数据
printf("我是子进程: a = %d\r\n",a);
exit(0);
default:
printf("我是父进程: a = %d\r\n",a);
exit(0);
}
}
使用vfork()创建子进程,子进程不会拷贝父进程中的数据,而是直接使用父进程中的数据。子进程中更改数据也影响父进程中的数据。
c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
pid_t pid;
int a = 10;
pid = vfork(); //vfork创建子进程
switch(pid)
{
case -1:
perror("fork error");
exit(-1);
case 0:
a+=10; //子进程中改变数据
printf("我是子进程: a = %d\r\n",a);
exit(0);
default:
printf("我是父进程: a = %d\r\n",a);
exit(0);
}
}
父子竞争
调用 fork()之后,子进程成为了一个独立的进程,可被系统调度运行,而父进程也继续被系统调度运行,
那么谁先访问cpu呢?答案是不确定的,父子进程运行顺序不确定。
从测试结果可知,虽然绝大部分情况下,父进程会先于子进程被执行,但是并不排除子进程先于父进程
被执行的可能性。
c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
pid_t pid;
pid = fork();
switch(pid)
{
case -1:
perror("fork error");
exit(-1);
case 0:
printf("我是子进程\r\n");
exit(0);
default:
printf("我是父进程\r\n");
exit(0);
}
}
进程退出
进程退出包括正常退出和异常退出:
正常退出
- main()函数中通过 return 语句返回来终止进程;
- 应用程序中调用 exit()函数终止进程;
- 应用程序中调用exit()或_Exit()终止进程;
- 补充:进程中最后一个线程返回,进程也会退出。最后一个线程调用pthrea_exit();
异常退出
-
应用程序中调用 abort()函数终止进程;
-
进程接收到一个信号,譬如 SIGKILL 信号。
一般使用 exit()库函数而非exit()系统调用 ,原因在于 exit()最终也会通过exit()终止进程,但在此之前,它将会完成一些其它的工作, exit()函数会执行的动作如下 :
- 如果程序中注册了进程终止处理函数,那么会调用终止处理函数。
- 刷新 stdio 流缓冲区 。
- 执行_exit()系统调用。
c
#include <stdlib.h>
void exit(int status); //传入状态码,用于标识为啥退出,0表示正常退出,-1表示异常退出
监视子进程
就是等待子进程退出。对于许多需要创建子进程的进程来说,有时设计需要监视子进程的终止时间以及终止时的一些状态信息,在某些设计需求下这是很有必要的。
wait()
系统调用 wait()可以等待进程的任一子进程终止,同时获取子进程的终止状态信息,wait()函数的作用除了获取子进程的终止状态信息之外,更重要的一点,就是回收子进程的一些资源,俗称为子进程"收尸" , 其函数原型如下所示:
c
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status);
参数介绍:
- status: 参数 status 用于存放子进程终止时的状态信息,参数 status 可以为 NULL,表示不接收子进程
终止时的状态信息。 - 返回值: 若成功则返回终止的子进程对应的进程号;失败则返回-1。
系统调用 wait()将执行如下动作: 一次 wait()调用只能处理一次。
-
调用 wait()函数,如果其所有子进程都还在运行,则 wait()会一直阻塞等待,直到某一个子进程终
止;
-
如果进程调用 wait(),但是该进程并没有子进程, 也就意味着该进程并没有需要等待的子进程, 那 么 wait()将返回错误,也就是返回-1、并且会将 errno 设置为 ECHILD。
-
如果进程调用 wait()之前, 它的子进程当中已经有一个或多个子进程已经终止了,那么调用 wait()
也不会阻塞,而是会立即替该子进程"收尸" 、处理它的"后事" 。
c#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/types.h> #include <sys/wait.h> #include <errno.h> int main(void) { int status; int ret; int i; /* 循环创建 3 个子进程 */ for (i = 1; i <= 3; i++) { switch (fork()) { case -1: perror("fork error"); exit(-1); case 0: /* 子进程 */ printf("子进程<%d>被创建\n", getpid()); sleep(i); _exit(i); default: /* 父进程 */ break; //跳出switch } } sleep(1); printf("~~~~~~~~~~~~~~\n"); for (i = 1; i <= 3; i++) { ret = wait(&status); if (-1 == ret) { if (ECHILD == errno) { printf("没有需要等待回收的子进程\n"); exit(0); } else { perror("wait error"); exit(-1); } } printf("回收子进程<%d>, 终止状态<%d>\n", ret, WEXITSTATUS(status)); } exit(0); }
waitpid()
用 wait()系统调用存在着一些限制,这些限制包括如下:
- 如果父进程创建了多个子进程,使用 wait()将无法等待某个特定的子进程的完成,只能按照顺序等待下一个子进程的终止,一个一个来、谁先终止就先处理谁;
- 如果子进程没有终止,正在运行,那么 wait()总是保持阻塞,有时我们希望执行非阻塞等待,是否有子进程终止,通过判断即可得知;
- 使用 wait()只能发现那些被终止的子进程,对于子进程因某个信号(譬如 SIGSTOP 信号)而停止(注意,这里停止指的暂停运行),或是已停止的子进程收到 SIGCONT 信号后恢复执行的情况就无能为力了
函数原型:
c
#include <sys/types.h>
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *status, int options);
参数介绍:
- pid: 参数 pid 用于表示需要等待的某个具体子进程,关于参数 pid 的取值范围如下:
- 如果 pid 大于 0,表示等待进程号为 pid 的子进程;
- 如果 pid 等于 0,则等待与调用进程(父进程)同一个进程组的所有子进程;
- 如果 pid 小于-1,则会等待进程组标识符与 pid 绝对值相等的所有子进程;
- 如果 pid 等于-1,则等待任意子进程。 wait(&status)与 waitpid(-1, &status, 0)等价。
- status: 与 wait()函数的 status 参数意义相同。
- options: 参数 options 是一个位掩码,可以包括 0 个或多个标志: WNOHANG;WUNTRACED ;WCONTINUED
- 返回值: 返回值与 wait()函数的返回值意义基本相同,在参数 options 包含了 WNOHANG 标志的情况
下,返回值会出现 0。
waitid()
waitid()与 waitpid()类似,不过 waitid()提供了更多的扩展功能,具体的使用方法笔者便不再介绍,大家有兴趣可以自己通过 查阅资料进行学习 。
孤儿进程
父进程先于子进程结束,也就是意味着,此时子进程变成了一个"孤儿",我们把这种进程就称为孤儿
进程。
在 Linux 系统当中,所有的孤儿进程都自动成为 init 进程(进程号为 1)的子进程。
僵尸进程
进程结束之后,通常需要其父进程为其"收尸",回收子进程占用的一些内存资源,父进程通过调用
wait()(或其变体 waitpid()、 waitid()等)函数回收子进程资源,归还给系统 。
如果子进程先于父进程结束,此时父进程还未来得及给子进程"收尸",那么此时子进程就变成了一个
僵尸进程。
另外一种情况,如果父进程并没有调用 wait()函数然后就退出了,那么此时 init 进程将会接管它的子进程并
自动调用 wait(), 故而从系统中移除僵尸进程。
系统中存在大量的僵尸进程,它们势必会填满内核进程表,从而阻碍新进程的创建。
需要注意的是,僵尸进程是无法通过信号将其杀死的,即使是"一击必杀"信号 SIGKILL 也无法将其杀死,那么这种情况下,只能杀死僵尸进程的父进程(或等待其父进程终止),这样 init 进程将会接管这些僵尸进程,从而将它们从系统中清理掉!
总结
- 孤儿进程: 父进程先结束**【爹先挂了】,** 被init养父(进程号为1) 收养,并被重新设置为其子进程
- 僵尸进程: 子进程终止,但父进程没有使用wait或waitpid收集其资源**【爹不管】**
- 守护进程: 在后台运行,不与任何终端关联的进程**【后台天使】**
守护进程
守护进程(Daemon) 也称为精灵进程,是运行在后台的一种特殊进程,它独立于控制终端并且周期性
地执行某种任务或等待处理某些事情的发生, 主要表现为以下两个特点:
- 长期运行。系统启动开始运行,除非强制终止,不然直到系统关机才停止运行。普通进程是用户登入或者程序运行时创建,到用户注销或程序退出时终止。但守护进程不受用户登录或注销的影响,一直运行。
- 与终端脱离。在 Linux 中,系统与用户交互的界面称为终端,每一个从终端开始运行的进程都
会依附于这个终端 。
守护进程 Daemon,通常简称为 d,一般进程名后面带有 d 就表示它是一个守护进程。 TTY列中是?号,表示没有控制终端,也就是守护进程。
exec簇函数
当子进程的工作不再是运行父进程的代码段,而是运行另一个新程序的代码(另一个可执行程序),那么这个时候子进程可以通过 exec 函数来实现运行另一个新的程序 。
说到这里,为什么需要在子进程中执行新程序?其实这个问题非常简单,虽然可以直接在子进程分支编写子进程需要运行的代码,但是不够灵活,扩展性不够好,直接将子进程需要运行的代码单独放在一个可执行文件中不是更好吗, 所以就出现了 exec 操作 。
exec 族函数中的库函数都是基于系统调用 execve()而实现的,虽然参数各异、但功能相同, 包括: execl()、 execlp()、 execle()、 execv()、execvp()、 execvpe(), 它们的函数原型如下所示:
c
#include <unistd.h>
extern char **environ;
int execl(const char *path, const char *arg, ... /* (char *) NULL */);
int execlp(const char *file, const char *arg, ... /* (char *) NULL */);
int execle(const char *path, const char *arg, ... /*, (char *) NULL, char * const envp[] */);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[], char *const envp[]);
execve()
execve()可以将新程序加载到某一进程的内存空间运行,使用新的程序替换旧的程序,而进程的栈、数据、以及堆数据会被新程序的相应部件所替换,然后从新程序的 main()函数开始执行。 函数原型如下:
c
#include <unistd.h>
int execve(const char *filename, char *const argv[], char *const envp[]);
返回值: execve 调用成功将不会返回;失败将返回-1,并设置 errno。
参数介绍:
- filename: 指向需要载入当前进程空间的新程序的路径名,既可以是绝对路径、也可以是相对路径。
- argv: 指定了传递给新程序的命令行参数。是一个字符串数组, 该数组对应于 main(int argc, char*argv[])函数的第二个参数 argv,且格式也与之相同,是由字符串指针所组成的数组,以 NULL 结束。argv[0]对应的便是新程序自身路径名。
- envp: 参数 envp 也是一个字符串指针数组, 指定了新程序的环境变量列表, 参数 envp 其实对应于新程序的 environ 数组,同样也是以 NULL 结束,所指向的字符串格式为 name=value。
使用试列:
下列execve()函数的使用并不是它真正的应用场景, 通常由 fork()生成的子进程对 execve()的调用最为频繁,也就是子进程执行 exec 操作; 试列 中的 execve 用法在实际的应用不常见,这里只是给大家进行演示说明。
testAPP程序中通过调用execve函数来执行newAPP程序
c
/*testAPP.c*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(int argc, char *argv[])
{
char *arg_arr[5];
char *env_arr[5] = {"NAME=app", "AGE=25",
"SEX=man", NULL}; //设置newAPP程序中的环境变量
if (2 > argc)
exit(-1);
arg_arr[0] = argv[1];
arg_arr[1] = "Hello";
arg_arr[2] = "World";
arg_arr[3] = NULL; //必须以NULL结束
execve(argv[1], arg_arr, env_arr);
perror("execve error"); //execve成功退出后,该程序也结束,并不会执行这些代码
exit(-1);
}
c
/*newAPP.c*/
#include <stdio.h>
#include <stdlib.h>
extern char **environ; //对应testAPP中的env_arr
int main(int argc, char *argv[])
{
char **ep = NULL;
int j;
for (j = 0; j < argc; j++)
{
printf("argv[%d]: %s\n", j, argv[j]); //打印传递过来的参数
}
puts("env:");
for (ep = environ; *ep != NULL; ep++)
{
printf(" %s\n", *ep); //打印环境变量
}
exit(0);
}
execl()
函数原型:
c
int execl(const char *path, const char *arg, ... );
参数介绍:
-
path:指向新可执行程序的路径,可以是相对和绝对路径
-
参数列表:把参数列表依次排列,使用可变参数形式传递,本质上也是多个字符串,以 NULL 结尾。
cexecl("./newApp", "./newApp", "Hello", "World", NULL);
execv
函数原型:
c
int execv(const char *path, char *const argv[]);
参数介绍:
-
path:指向新可执行程序的路径,可以是相对和绝对路径。
-
argv:指定了传递给新程序的命令行参数。是一个字符串数组, 该数组对应于 main(int argc, char*argv[])函数的第二个参数 argv,且格式也与之相同,是由字符串指针所组成的数组,以 NULL 结束。argv[0]对应的便是新程序自身路径名。
cchar *arg_arr[5]; arg_arr[0] = "./newApp"; arg_arr[1] = "Hello"; arg_arr[2] = "World"; arg_arr[3] = NULL; execv("./newApp", arg_arr);
execlp()execvp()
c
int execlp(const char *file, const char *arg, ... );
int execvp(const char *file, char *const argv[]);
execlp()和 execvp()在 execl()和 execv()基础上加了一个 p,这个 p 其实表示的是 PATH。execl()和execv()要求提供新程序的路径名,而 execlp()和 execvp()则允许只提供新程序文件名,系统会在由
环境变量 PATH 所指定的目录列表中寻找相应的可执行文件,如果执行的新程序是一个 Linux 命令,这将很有用; 当然, execlp()和 execvp()函数也兼容相对路径和绝对路径的方式。
execle()与execvpe()
c
int execle(const char *path, const char *arg, ... );
int execvpe(const char *file, char *const argv[], char *const envp[]);
execle()和 execvpe()这两个函数在命名上加了一个 e,这个 e 其实表示的是 environment 环境变量,
意味着这两个函数可以指定自定义的环境变量列表给新程序, 参数envp与系统调用execve()的envp
参数相同,也是字符串指针数组 。
c
//execvpe 传参
char *env_arr[5] = {"NAME=app", "AGE=25","SEX=man", NULL};
char *arg_arr[5];
arg_arr[0] = "./newApp";
arg_arr[1] = "Hello";
arg_arr[2] = "World";
arg_arr[3] = NULL;
execvpe("./newApp", arg_arr, env_arr);
c
// execle 传参
execle("./newApp", "./newApp", "Hello", "World", NULL, env_arr);
使用exec簇函数执行ls命令
execl
c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(void)
{
execl("/bin/ls", "ls", "-a", "-l", NULL);
perror("execl error");
exit(-1);
}
execv
c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(void)
{
char *arg_arr[5];
arg_arr[0] = "ls";
arg_arr[1] = "-a";
arg_arr[2] = "-l";
arg_arr[3] = NULL;
execv("/bin/ls", arg_arr);
perror("execv error");
exit(-1);
}
execlp
c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(void)
{
execlp("ls", "ls", "-a", "-l", NULL);
perror("execlp error");
exit(-1);
}
execvp
c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(void)
{
char *arg_arr[5];
arg_arr[0] = "ls";
arg_arr[1] = "-a";
arg_arr[2] = "-l";
arg_arr[3] = NULL;
execvp("ls", arg_arr);
perror("execvp error");
exit(-1);
}
execle
c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
extern char **environ;
int main(void)
{
execle("/bin/ls", "ls", "-a", "-l", NULL, environ);
perror("execle error");
exit(-1);
}
execvpe
c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
extern char **environ;
int main(void)
{
char *arg_arr[5];
arg_arr[0] = "ls";
arg_arr[1] = "-a";
arg_arr[2] = "-l";
arg_arr[3] = NULL;
execvpe("ls", arg_arr, environ);
perror("execvpe error");
exit(-1);
}
system函数
使用 system()函数可以很方便地在我们的程序当中执行任意 shell 命令 。system()函数其内部的是通过调用 fork()、execl()以及 waitpid()这三个函数来实现它的功能。
函数原型:
c
#include <stdlib.h>
int system(const char *command);
返回值:
-
当参数 command 为 NULL, 如果 shell 可用则返回一个非 0 值,若不可用则返回 0;针对一些非
UNIX 系统,该系统上可能是没有 shell 的,这样就会导致 shell 不可能;如果 command 参数不为
NULL,则返回值从以下的各种情况所决定。
-
如果无法创建子进程或无法获取子进程的终止状态,那么 system()返回-1;
-
如果子进程不能执行 shell,则 system()的返回值就好像是子进程通过调用_exit(127)终止了;
-
如果所有的系统调用都成功, system()函数会返回执行 command 的 shell 进程的终止状态。
参数介绍:
- command: 指向需要执行的 shell 命令,以字符串的形式提供,譬如"ls -al"、 "echo
HelloWorld"等 。
使用示例:将需要执行的命令通过参数传递给main函数,main函数里调用system来执行该命令。
c
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char *argv[])
{
int ret;
if (2 > argc)
exit(-1);
ret = system(argv[1]);
if (-1 == ret)
{
fputs("system error.\n", stderr);
}
else
{
if (WIFEXITED(ret) && (127 == WEXITSTATUS(ret)))
fputs("could not invoke shell.\n", stderr);
}
exit(0);
}
popen函数
也是用于执行shell命令的函数,与system相比,popen能够将执行命令后得到的数据通过管道进行读出或写入。
popen() 函数通过创建一个管道,调用 fork 产生一个子进程,执行一个 shell 以运行命令来开启一个进程。
这个进程必须由 pclose() 函数关闭,而不是 fclose() 函数。pclose() 函数关闭标准 I/O 流,等待命令执行结束,然后返回 shell 的终止状态。如果 shell 不能被执行,则 pclose() 返回的终止状态与 shell 已执行 exit 一样。也就是,popen创建管道,执行shell命令将文件流中的某些数据读出。
函数原型:
c
#include <stdio.h>//头文件
FILE *popen(const char *command, const char *type);
补充:FILE指针,相当于文件描述符fd,作为文件句柄。
参数介绍:
- command:是一个指向以 NULL 结束的 shell 命令字符串的指针。命令将被传到 bin/sh 并使用 -c 标志,shell 将执行这个命令,比如sh -c ls。
- type:"r" 则文件指针连接到 command 的标准输出;如果 type 是 "w" 则文件指针连接到 command 的标准输入。
使用示例:通过调用popen去执行ls -l命令,把结果给fp,通过fread读取
c
#include <stdio.h>
#include <unistd.h>
int main(int argc ,char **argv){
char ret[1024] = {0};
FILE *fp;
fp = popen("ls -l","r");
//size_t fwrite(const void *ptr, size_t size, size_t nmemb,FILE *stream);
int nread = fread(ret,1,1024,fp);
printf("read ret %d byte, %s\n",nread,ret);
return 0;
}