目录
进程相关概念
如何查看系统中有哪些进程?
1)在Ubuntu下,使用ps -aux指令查看系统当前有哪一些进程。
实际工作中,配合grep来查找程序中是否存在某一个进程。
例如:
在终端输入ps -aux|grep init
系统会输出带有init字段相关的进程
2)使用top指令查看,类似windows任务管理器
什么是进程标识符?
每个进程都有一个非负整数表示的唯一ID,叫做pid,类似身份证
调用getpid( )函数获取自身的进程标识符,调用getppid获取父进程的进程标识符
#include<stdio.h>
#include<sys/types.h>
#include<unistd.h>
int main(void)
{
printf("pid = %d\n",getpid());
while(1);
return 0;
}
什么叫父进程,什么叫子进程?
进程A创建了进程B
那么A叫做父进程,B叫做子进程,父子进程是相对的概念,理解为人类中的父子关系。
C程序的存储空间如何分配?
fork( )函数
函数原型:pid_t fork(void);
函数返回值:调用成功时,在父进程中,子进程的PID作为函数的返回值。在子进程中,0作为函数的返回值。如果调用失败,则返回-1。
#include<stdio.h>
#include<sys/types.h>
#include<unistd.h>
int main(void)
{
printf("pid = %d\n",getpid());
fork(); //调用fork函数创建子进程
printf("pid = %d\n",getpid());// 这段代码会被执行两次,父进程一次,子进程一次
return 0;
}
fork( )函数的实际使用场景
fork( )创建一个子进程的一般目的:
1)一个父进程希望复制自己,使父,子进程同时执行不同的代码段。这在网络服务进程中是常见的----父进程等待客户端的服务请求。当这种请求到达时,父进程调用fork,使子进程处理此请求。父进程则继续等待下一个服务请求到达。
2)一个进程要执行一个不同的程序。这对shell是常见的情况。在这种情况下,子进程从fork返回后立即调用exec。
fork()函数总结:
一个现有进程可以调用fork函数创建一个新进程。
fork( )函数的返回值:子进程中返回0,父进程中返回子进程ID,出错返回-1。
由fork创建的新进程被称为 子进程 (child process)。fork函数被调用一次,但返回两次。两次返回的唯一区别是子进程的返回值是0,而父进程的返回值则是新子进程的进程ID。将子进程ID返回给父进程的理由是:因为一个进程的子进程可以有多个,并且没有一个函数使一个进程可以获得其所有子进程的进程ID。fork使子进程得到返回值0的理由是:一个进程只会有一个父进程,所以子进程总是可以调用getppid以获得其父进程的进程ID(进程ID 0总是由内核交换进程使用,所以一个子进程的进程ID不可能为0)。
子进程和父进程继续执行fork调用之后的指令。子进程是父进程的副本。例如,子进程获得父进程数据空间,堆和栈的副本。注意,这是子进程所拥有的副本。父,子进程并不共享这些存储空间部分。父,子进程共享正文段。
由于在fork之后经常跟随exec,所以现在的很多实现并不执行一个父进程数据段,栈和堆的完全复制。作为替代,使用了写时复制(copy-on-write,COW)技术。这些区域由父,子进程共享,而内核将它们的访问权限改变为只读的。如果父,子进程中的任一个试图修改这些区域,则内核只为修改区域的那块内存制作一个副本,通常是虚拟存储器系统中的一“页”。
fork()函数练习
阅读下面的代码,显示终端会打印出几个hello world?
#include<sys/types.h>
#include<unistd.h>
#include<stdio.h>
#include<stdlib.h>
int main(void)
{
fork();
fork();
fork();
printf("hello world!\n");
exit(-1);
return 0;
}
fork嵌套分析
第一个fork执行了1次。第二fork执行了2次,第三个fork执行了4次。最终有8个进程运行程序。从而printf函数被调用了8次。
在PC上验证一下,输出结果如下:
vfork( )函数
vfork( )函数也可以创建进程,与fork()函数有一下两点区别:
1)vfork( )直接使用父进程存储空间,不拷贝。
2)vfork( )保证子进程先运行,当子进程调用exit退出后,父进程才执行。
/*vfork.c -- vfork()函数的使用
1) vfork函数保证子进程先运行,当子进程调用exit退出后,父进程才执行
2) vfork直接使用父进程存储空间,不拷贝。
*/
#include<stdio.h>
#include<sys/types.h>
#include<unistd.h>
#include<stdlib.h>
int main(void)
{
pid_t ret;
int cnt = 0;
ret = vfork();
if( 0 == ret)
{
while(1)
{
printf("this is child process\n");
sleep(1);
if(++cnt == 3)
{
exit(0);//使用exit()才能正常退出,如果使用break退出循环会把变量cnt的值破坏
}
}
}
else
{
while(1)
{
printf("this is father process\n");
printf("cnt = %d\n",cnt);
sleep(1);
}
}
return 0;
}
进程退出
正常退出:
1)main函数调用return
2)进程调用exit( ),标准c库
3)进程调用_exit( )或者_Exit( ),属于系统调用
4)进程最后一个线程返回
5)最后一个线程调用pthread_exit
异常退出:
1)调用abort
2)当进程收到某些信号时,如ctrl + C
3) 最后一个线程对取消(cancellation)请求做出响应
不管进程如何终止,最后都会执行内核中的同一段代码。这段代码为相应进程关闭所有打开描述符,释放它所使用的存储器等。
对上述任意一种终止情形,我们都希望终止进程能够通知其父进程它是如何终止的。对于三个终止函数(exit,_exit和_Exit),实现这一点的方法是,将其退出状态(exit status)作为参数传送给函数。在异常终止情况下,内核(不是进程本身)产生一个指示其异常终止原因的终止状态(termination status)。在任意一种情况下,该终止进程的父进程都能用wait或waitpid函数取得其终止状态。
什么是僵尸进程?
僵尸进程是当子进程比父进程先结束,而父进程又没有回收子进程,释放子进程占用的资源,此时子进程将成为一个僵尸进程。
示例:在下面的代码段中,子进程比父进程先结束,但是子进程的退出状态未被收集,此时该子进程就编程了僵尸进程
#include<stdio.h>
#include<sys/types.h>
#include<unistd.h>
int main(void)
{
pid_t ret;
ret = fork(); //调用fork函数创建子进程
if( ret == 0)
{
printf("this is child process\n");
}
else
{
while(1)
{
printf("this is father process\n");
sleep(1);
}
}
return 0;
}
wait( )函数的使用
函数原型:pid_t wait(int *status);
参数说明:status为空,不关心子进程退出状态;status非空,子进程退出状态放在它所指向的地址中。
函数说明:1)如果其所有子进程都还在运行,则阻塞。
2)如果一个子进程已终止,正等待父进程获取其终止状态,则取得该子进程的终止状 态立即返回。
3)如果它没有任何子进程,则立即出错返回。
情况1:status为空时,父进程不关心子进程退出状态
#include <sys/wait.h>
#include<stdio.h>
#include<sys/types.h>
#include<unistd.h>
int main(void)
{
pid_t ret;
ret = fork(); //调用fork函数创建子进程
int cnt = 6;
if( ret == 0)
{
while(cnt--){
printf("this is child process\n");
sleep(1);
}
}
else
{
wait(NULL); //父进程在此处阻塞等待子进程退出
while(1)
{
printf("this is father process\n");
sleep(1);
}
}
return 0;
}
说明: 在上面的代码中,父进程在wait( )函数调用处阻塞,并等待子进程退出。
情况2:status为非空,子进程的退出状态放在他所指向的地址中
#include <sys/wait.h>
#include<stdio.h>
#include<sys/types.h>
#include<unistd.h>
#include<stdlib.h>
int main(void)
{
pid_t ret;
ret = fork(); //调用fork函数创建子进程
int status;
int cnt = 6;
if( ret == 0)
{
while(cnt--){
printf("this is child process\n");
sleep(1);
}
exit(23); //退出状态不能为负数,否则打印结果会不一样
}
else
{
wait(&status); //父进程在此处阻塞等待子进程退出
printf("exit_status = %d\n",WEXITSTATUS(status));
while(1)
{
printf("this is father process\n");
sleep(1);
}
}
return 0;
}
说明:exit( )的退出码如果为负数,打印的结果会不一样;如果子进程正常退出,对于这种情况可执行WEXITSTATUS(status),取子进程传送给exit,_exit或_Exit参数的低8位。
waitpid( )函数的使用
waitpid( )函数和wait( )函数都是用于等待子进程退出。区别:wait( )函数使调用者阻塞,waitpid( )函数有一个选项,可以使调用者不阻塞。
函数原型:pid_t waitpid(pid_t pid, int *status, int options);
参数说明:
status参数:
是一个整形数指针,非空:子进程退出状态放在它所指向的地址中。空:不关心退出状态。
pid参数:
pid == -1 | 等待任一子进程。就这一方面而言,waitpid与wait等效 |
pid > 0 | 等带其进程ID与pid相等的子进程 |
pid == 0 | 等待其组ID等于调用进程组ID的任一子进程。 |
pid < -1 | 等待其组ID等于pid绝对值的任一子进程 |
option参数:
WCONTINUED | 若实现支持作业控制,那么由pid指定的任一子进程在暂停后已经继续,但其状态尚未报告,则返回其状态 |
WNOHANG | 若由pid指定的子进程并不是立即可用的,则waitpid不阻塞,此时其返回值为0 |
WUNTRACED | 若某实现支持作业控制,而由pid指定的任一子进程已处于暂停状态,并且其状态自暂停以来还未报告过,则返回其状态。WIFSTOPPED宏确定返回值是否对应于一个暂停子进程 |
示例:
#include<sys/wait.h>
#include<stdio.h>
#include<sys/types.h>
#include<unistd.h>
#include<stdlib.h>
int main(void)
{
pid_t ret;
int cnt = 6;
int status;
ret = fork(); //调用fork函数创建子进程
if( 0 == ret)
{
while(cnt--)
{
printf("this is child process!\n");
sleep(1);
}
exit(12);
}
else
{
waitpid(ret,&status,WNOHANG);// 使用WNOHANG不阻塞等待子进程退出
printf("exit status:%d\n",WEXITSTATUS(status));
while(1)
{
printf("this is father process!\n");
sleep(1);
}
}
return 0;
}
孤儿进程
父进程如果不等待子进程退出,在子进程之前就结束了自己的“生命”,此时子进程叫做孤儿进程。
linux避免系统存在过多孤儿进程,init进程收留孤儿进程,变成孤儿进程的父进程。
exec族函数
我们调用fork函数创建新进程后,经常会在新进程中调用exec函数去执行另外一个程序。该进程被完全替换为新程序。因为调用exec函数并不创建新进程,所有前后进程的ID并没有改变。
功能:
在调用进程内部执行一个可执行文件。可执行文件即可以是二进制文件,也可以是任何linux下可执行的脚本文件。
返回值:
exec族函数执行成功后不会返回,调用失败时,会设置errno(可以使用perror函数将错误的信息打印出来)并返回-1,然后从原程序的调用点接着往下执行。
execl( )函数使用示例:
exec函数原型:
int execl(const char * path,const char * arg,…);
参数说明:
path:要执行的程序路径。
arg :程序的第0个参数,即程序名自身。相当于argv【0】。
... :命令行参数列表。调用相应程序时有多少命令行参数,就需要有多少个输入参数项。注意:在使用此类函数时,在所有命令行参数的最后应该增加一个空的参数项(NULL),表明命令行参数结束。
echoargv.c文件代码如下:
#include<stdio.h>
int main(int argc, char *argv[])
{
int i;
for(i = 0; i < argc; i++)
{
printf("argv[%d]:%s\n",i,argv[i]);
}
return 0;
}
1)使用execl( )函数去调用echoargv程序
#include<stdio.h>
#include<unistd.h>
// 函数原型: int execl(const *path, const char *arg,...);
int main(int argc, char *argv[])
{
printf("before execl\n");
if(execl("./echoargv","echoargv","abc",NULL) == -1)
{
printf("execl failed!\n");
perror("reason:"); //如果调用失败,就将错误的信息打印出来
}
printf("after execl\n"); //如果调用程序成功,不会执行这句代码
return 0;
}
2)使用execl调用系统 ls 指令,先使用whereis指令查看 ls指令存放的路径
#include<stdio.h>
#include<unistd.h>
// 函数原型: int execl(const *path, const char *arg,...);
int main(int argc, char *argv[])
{
printf("before execl\n");
if(execl("/usr/bin/ls","ls","-l",NULL) == -1)
{
printf("execl failed!\n");
perror("reason:"); //如果调用失败,就将错误的信息打印出来
}
printf("after execl\n"); //如果调用程序成功,不会执行这句代码
return 0;
}
3)使用execl调用系统date指令,先使用whereis指令查看date指令存放的位置
#include<stdio.h>
#include<unistd.h>
// 函数原型: int execl(const *path, const char *arg,...);
int main(int argc, char *argv[])
{
printf("the system time is \n");
if(execl("/usr/bin/date","date",NULL) == -1)
{
printf("execl failed!\n");
perror("reason:"); //如果调用失败,就将错误的信息打印出来
}
printf("after execl\n"); //如果调用程序成功,不会执行这句代码
return 0;
}
execlp( )函数使用示例:
函数原型:int execlp(const char * file,const char * arg,…);
execlp( )函数与execl( )函数使用类似,唯一区别是execlp( )函数能通过环境变量PATH查找到可执行文件。
1)使用execlp调用系统指令date,不需要添加绝对路劲名。
#include<stdio.h>
#include<unistd.h>
int main(int argc, char *argv[])
{
printf("the system time is \n");
if(execlp("date","date",NULL) == -1)
{
printf("execl failed!\n");
perror("reason:"); //如果调用失败,就将错误的信息打印出来
}
printf("after execl\n"); //如果调用程序成功,不会执行这句代码
return 0;
}
execvp( )函数使用示例:
函数原型:int execvp(const char *file, char *const argv[ ]);
区别:单独定义一个指针数组,里面存放参数。调用时,将指针数组名作为参数进行调用。
#include<stdio.h>
#include<unistd.h>
int main(int argc, char *argv[])
{
char *arg[] = {"ls","-l",NULL};
if(execvp("ls",arg) == -1)
{
printf("execl failed!\n");
perror("reason:"); //如果调用失败,就将错误的信息打印出来
}
printf("after execl\n"); //如果调用程序成功,不会执行这句代码
return 0;
}
execv( )函数使用示例:
函数原型:int execv(const char *path, char *const argv[ ]);
区别:单独定义一个指针数组,里面存放参数。调用时,将指针数组名作为参数进行调用。同时需要将可执行文件的绝对路径写出。
#include<stdio.h>
#include<unistd.h>
int main(int argc, char *argv[])
{
char *arg[] = {"ls","-l",NULL};
if(execv("/usr/bin/ls",arg) == -1)
{
printf("execl failed!\n");
perror("reason:"); //如果调用失败,就将错误的信息打印出来
}
printf("after execl\n"); //如果调用程序成功,不会执行这句代码
return 0;
}
system( )函数
system( )函数的返回值如下:成功,则返回进程的状态值;当sh不能执行时,返回127;失败返回-1;
实际上system( )函数执行了三步操作:
1)fork一个子进程
2)在子进程中调用exec函数去执行cmd;
3)在父进程中调用wait去等待子进程结束。
#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h>
#include<stdio.h>
#include<stdlib.h>
int system(const char *cmd);
int main(void)
{
int status = system("ps");
printf("status = %d\n",status);
return 0;
}
int system(const char *cmd)
{
pid_t pid;
int status;
if( cmd == NULL)
{
return 1;
}
if((pid = fork()) < 0)
{
status = -1;
}
if( 0 == pid)
{
execl("/bin/sh","sh","-c",cmd,NULL);
exit(127); //exec执行失败返回127,注意exec只在失败时才返回现在的进程,成功的话当前进程就不存在了
}
else
{
while(waitpid(pid,&status,0) < 0)
{
if(errno != EINTER)
{
status = -1; // 如果waitpid被信号中断,则返回-1
break;
}
}
}
return status;//如果waitpid成功,则返回子进程的返回状态
}
popen( )函数
函数原型:
FILE *popen(const char *command, const char *type);
参数说明:
command是需要执行的指令,type是"r"或者是"w".表示可读或者可写。
返回值:
成功则返回文件指令,失败返回NULL,错误原因存在errno。
#include<stdio.h>
#include<stdlib.h>
int main(void)
{
FILE *fd;
int n_read;
char buffer[1024] = {'\0'};
int size;
fd = popen("ls -l","r");//执行ps指令,并将结果重定位到管道
if( NULL == fd)
{
perror("why:");
}
size = fseek(fd,0,SEEK_END);
fseek(fd,0,SEEK_SET);
n_read = fread(buffer,1,size,fd); //读取管道中的数据
if(n_read == -1)
{
printf("read error!\n");
exit(-1);
}
pclose(fd); // popen的返回值是个标准I/O流,必须由pclose来关闭管道
printf("%s\n",buffer);
return 0;
}