计算机系统的各种硬件资源是有限的,在现代多任务操作系统上同时运行的多个进程都需要访问这些资源。为了更好的管理有限的资源,所有对这些资源的访问必须由操作系统控制,不允许进程直接操作。因此操作系统提供了使用这些资源的唯一入口----系统调用(System Call),它是操作系统向用户程序提供的一种申请操作系统服务的接口。在linux中系统调用是用户空间访问内核的唯一手段,除异常和陷入外,他们是内核唯一的合法入口。下面将讲解几个常见的进程控制与通信相关的系统调用函数。
关于Linux进程,有以下必要的知识需要了解:
#include
pid_t fork(void);
作用:创建一个新的进程,让这个进程成为当前进程的子进程。这两个进程执行没有固定的先后顺序,哪个进程先执行要看系统的进程调度策略。
fork函数是一个系统调用,在调用fork函数时,操作系统会复制当前进程的所有信息(包括打开文件、信号处理等),并将其分配给新的进程,但是有些资源是共享的,例如内存映射、消息队列等。新进程与原进程几乎完全相同,但也有略微不同:
fork的返回值不同:在原进程中,fork返回值为新进程的pid;在新进程中,fork返回值为0;如果出现错误,返回一个负值;因此,在程序中可以根据返回值来判断当前代码运行在哪个进程中。
使用案例:
#include
#include
#include
int main(){
pid_t pid;
pid = fork();
if (pid <0){
printf("Error: fork failed.");
} else if (pid ==0){
printf("This is child process\n");
} else {
printf("This is parent process\n");
}
return 0;
}
由运行结果可以看出,当执行完 pid = fork(); 语句后,该语句下面的代码,子进程也会拥有,两个进程都会运行。
下面列举一个典型的例子,加深对fork函数的理解。给出如下c程序,在linux下使用gcc编译:
#include "stdio.h"
#include "sys/types.h"
#include "unistd.h"
int main()
{
pid_t pid1;
pid_t pid2;
pid1 = fork();
pid2 = fork();
printf("pid1:%d, pid2:%d\n", pid1, pid2);
}
已知从这个程序执行到这个程序的所有进程结束这个时间段内,没有其它新进程执行,请分析:
思考这个问题的关键是正确的区分 pid = fork(); 这段代码的上下两部分。
如图表示一个含有fork的程序,而fork语句可以看成将程序切为A、B两个部分,然后整个程序会如下运行:
参照上面的分析过程,下面一起分析刚刚的两个问题:
因此一共执行了四个进程,P0,P1,P2,P3,另外几个进程的输出分别为:
pid1:1001, pid2:0
pid1:0, pid2:1003
pid1:0, pid2:0
#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 execvpe(const char *file, char *const argv[],char *const envp[]);
参数说明:
path:可执行文件的路径名字
arg:可执行程序所带的参数,第一个参数为可执行文件名字,没有带路径且arg必须以NULL结束
file:如果参数file中包含/,则就将其视为路径名,否则就按 PATH环境变量,在它所指定的各目录中搜寻可执行文件。
exec族函数参数极难记忆和分辨,函数名中的字符会给我们一些帮助:
l : 使用参数列表
p:使用文件名,并从PATH环境进行寻找可执行文件
v:应先构造一个指向各参数的指针数组,然后将该数组的地址作为这些函数的参数。
e:多了envp[]数组,使用新的环境变量代替调用进程的环境变量,一般为NULL即可。
exec函数族作用:在进程中启动另一个程序。它可以根据指定的文件名或目录名找到可执行文件,并用它来取代原调用进程的数据段、代码段和堆栈段。在执行成功之后,原调用进程的内容除了进程号外,其他全部都被替换了。
成功无返回值,失败返回 -1。所以通常我们直接在exec函数调用后直接调用perror(),和exit(),无需if判断。
何时使用exec?
常用的也就是下面这两个:
使进程执行某一程序。成功无返回值,失败返回 -1
int execlp(const char *file, const char *arg, ...);
借助 PATH 环境变量找寻待执行程序
参1: 程序名
参2: argv0
参3: argv1
...: argvN
最后:NULL
该函数需要配合PATH环境变量来使用,当PATH所有目录搜素后没有参数1则返回出错。
该函数通常用来调用系统程序。如ls、date、cp、cat命令。
int execl(const char *path, const char *arg, ...);
自己指定待执行程序路径。
这两个函数的作用简单来说就是:execlp(),让当前进程或者子进程执行系统命令,比如:ls,cat,cp等命令;而 execl() 则是执行自己所有的可执行程序,比如一个c程序。
示例:
#include
#include
#include
#include
using namespace std;
int main(int argc, char **argv)
{
cout << "---------------" << endl;
pid_t pid;
pid = fork();
if(pid == -1)
{
perror("fork error");
exit(1);
}
else if(pid == 0)
{
execlp("ls", "ls", "-l", NULL); //执行命令:ls -l
//函数用法,参数1就是你要执行的命令名称,参数二也是,后面的参数是你执行命令添加的额外参数
//如果没有出错,就执行程序,不会有返回值,如果出错就返回错误值
perror("execlp error");
exit(1);
}
else if(pid > 0)
{
sleep(1);
cout << "my is prent pid = " << getpid() << endl;
}
return 0;
}
孤儿进程:父进程结束,子进程被init进程收养。
僵尸进程:子进程结束,父进程没有回收子进程的资源(PCB),这个资源必须要由父进程回收,否则就形成僵尸进程。
孤儿进程示例:
#include
#include
#include
#include
int main(int argc, char* argv[])
{
pid_t pid = fork();
if(pid == 0)
{
while(1)
{
printf("child: %d, ppid: %d\n", getpid(), getppid());
sleep(1);
}
}
if(pid > 0)
{
printf("parent: %d\n", getpid());
sleep(3);
}
return 0;
}
我们看到,子进程的父进程ID在3秒后变成了1,这说明父进程结束后,它变成了孤儿进程,并被init进程收养,使用kill命令基于可以杀死孤儿进程。
僵尸进程示例:
#include
#include
#include
#include
int main(int argc, char* argv[])
{
pid_t pid = fork();
if(pid == 0)
{
printf("child: %d, ppid: %d\n", getpid(), getppid());
sleep(1);
}
if(pid > 0)
{
while(1)
{
printf("parent: %d\n", getpid());
sleep(1);
}
}
return 0;
}
运行后,通过 ps -au 命令查看进程信息:
图中标出的Z+、defunct都可以表明这是僵尸进程。僵尸进程是不能用kill杀死的,因为kill命令是终止进程,而僵尸进程已经终止了。我们知道僵尸进程的资源需要由父进程去回收,那么我们在这种情况下如何回收僵尸进程的资源呢?方法就是杀死父进程,父进程被杀死后,由init接管子进程并回收子进程资源。
进程退出时,内核释放该进程大部分资源,包括打开的文件、占用的内存等。但仍保留了该进程的PCB信息,因此需要父进程通过 wait 和 waitpid 函数来进一步回收,否则这些进程会变成僵尸进程,消耗系统资源。
#include
#include
pid_t wait(int* status);
/*
参数:
status:存储进程退出时的状态信息;NULL则表示不关心子进程退出状态。
返回值:
成功:被回收的子进程号
失败:-1
*/
wait()作用:阻塞等待子进程终止,并获取子进程终止的原因,回收子进程资源。
Linux提供了一些非常有用的宏来帮助解析status,判断子进程终止原因,常用的有:
使用示例:
#include
#include
#include
#include
#include
int main(int argc, char* argv[])
{
pid_t pid = fork();
if(pid == 0)
{
printf("child: %d, ppid: %d\n", getpid(), getppid());
sleep(3); /*子进程睡眠3秒,那么父进程中的wait函数会阻塞3秒,一直等到子进程退出*/
return 66; /*正常退出,这个值可以被WEXITSTATUS获取到,这个值是有范围的*/
/*exit(66); 也表示正常退出*/
}
if(pid > 0)
{
int status;
pid_t wpid = wait(&status);
printf("wpid: %d, cpid: %d\n", wpid, pid);
if(WIFEXITED(status)) /*进程正常退出,获取退出原因*/
{
printf("child exit because: %d\n", WEXITSTATUS(status));
}
else /*非正常退出*/
{
if(WIFSIGNALED(status)) /*为真表示被信号杀死*/
{
printf("signal is: %d", WTERMSIG(status));
}
else
{
printf("other...\n");
}
}
while(1)
{
sleep(3);
}
}
return 0;
}
#include
#include
pid_t waitpid(pid_t pid, int* status, int options);
/*
功能:
等待子进程结束,回收该子进程资源,并传出子进程退出的状态到status。
参数:
pid:
> 0:等待进程号为pid的子进程退出;
= 0:等待同一个进程组中的任何子进程退出;若子进程已加入其他进程组,则不会等待;
= -1:等待任意一个子进程退出,此时和等价于wait函数;
< -1:等待进程组号为pid绝对值的进程组中的任何子进程退出。
status:存储进程退出时的状态信息;NULL表示不关心子进程退出状态。
options:
0:阻塞父进程,等待子进程退出。此时同wait函数。
WNOHANG:若无任何子进程退出,则立即返回。
WUNTRACED:若子进程暂停,则立即返回。(少用)
返回值:
a) 有子进程退出时,waitpid返回已收集到的退出子进程的进程号;
b) 若options设为WNOHANG,调用waitpid时无子进程退出,则返回0;
c) 若调用中出错,返回-1,同时设置errno.
如当pid对应的进程不存在,或pid对应的进程不是调用waitpid进程的子进程,就会出错,此时errno
被设为ECHILD。
*/
#include
#include
#include
#include
#include
int main(int argc, char* argv[])
{
int i = 0;
pid_t pid;
for(i = 0; i < 5; i++)
{
pid = fork();
if(pid == 0)
{
printf("child: %d\n", getpid());
break;
}
}
sleep(i);
if(i == 5) /*只有父进程可以执行到i=5*/
{
for(i = 0; i < 5; i++)
{
pid_t wpid = wait(NULL);
printf("wpid: %d\n", wpid);
}
while(1)
{
sleep(1);
}
}
return 0;
}
#include
#include
#include
#include
#include
int main(int argc, char* argv[])
{
int i = 0;
pid_t pid;
for(i = 0; i < 5; i++)
{
pid = fork();
if(pid == 0)
{
break;
}
}
if(i == 5) /*只有父进程可以执行到i=5*/
{
printf("parent: %d\n", getpid());
while(1) /*无限循环保证所有子进程全部回收*/
{
pid_t wpid = waitpid(-1/*回收任何子进程*/, NULL, WNOHANG);
if(wpid == -1)
{
break; /*如果返回-1说明已经没有子进程了,退出循环*/
}
if(wpid > 0)
{
printf("wpid: %d\n", wpid); /*打印被回收的子进程的ID*/
}
}
while(1)
{
sleep(1);
}
}
if(i < 5) /*说明是子进程*/
{
printf("no. %d child: %d\n", i, getpid());
}
return 0;
}
#include
int system(const char *command);
作用:用于执行一个系统命令或脚本。该函数会调用fork()产生子进程,因此程序将启动一个新的进程来运行。
system函数在Linux上的源码如下:
int system(const char * cmdstring)
{
pid_t pid;
int status;
if(cmdstring == NULL){
return (1);
}
if((pid = fork())<0){
status = -1;
}
else if(pid == 0){
execl("/bin/sh", "sh", "-c", cmdstring, (char *)0);
-exit(127); //子进程正常执行则不会执行此语句
}
else{
while(waitpid(pid, &status, 0) < 0){
if(errno != EINTER){
status = -1;
break;
}
}
}
return status;
}
常见用法:
system函数的返回值有多种情况,通过源码可以发现,其status来自waitpid函数 。判断一个 system 函数调用 shell 脚本是否正常结束的方法应该是如下 3 个条件同时成立:
使用system函数时要考虑参数的安全性,确保参数是可信的,以防止恶意代码注入。
在上一章system函数的源码中我们发现,system函数的执行需要通过调用 fork 函数创建一个子进程,子进程再通过execl函数调用shell对传参的可执行文件进行实现。这也意味着system函数实现需要依赖execl函数实现自身功能。因此system函数的结果将直接显示在终端上,这样原本运行的结果就无法在程序中用于实现信息交互等功能。
popen 函数与 system 函数的功能类似,它会调用fork创建一个子进程,之后创建一个连接到子进程的管道,然后读其输出或向其输入端发送数据,可以在程序中获取运行的输出结果。
#include
FILE *popen(const char *command, const char *type);
int pclose(FILE *stream);
参数说明:
commmand:是一个指向以 NULL 结束的 shell 命令字符串的指针。这行命令将被传到 bin/sh 并使用 -c 标志,shell 将执行这个命令。
type:只能是读和写的一种,如果是 “r” 则文件指针连接到command的标准输出,则返回的文件指针是可读的;如果是 “w” 则文件指针连接到command的标准输入,则返回的文件指针是可写的。
stream:popen返回的文件指针。
使用示例:
#include
int main()
{
FILE *File;
char readBuf[1024] = {0};
File = popen("ls","r");
fread(readBuf,1024,1,File);
printf("%s\n",readBuf);
return 0;
}
如果使用popen函数在 “w” 模式下,popen执行完后,仍然可以向管道继续传送命令,继续往标准I/O流中写入内容,直到调用pclose关闭它。