进程学习笔记

目录

进程相关概念

fork( )函数 

fork( )函数的实际使用场景

fork()函数练习

 vfork( )函数

进程退出

什么是僵尸进程?

wait( )函数的使用

waitpid( )函数的使用

孤儿进程

exec族函数

system( )函数

popen( )函数


进程相关概念

如何查看系统中有哪些进程?

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; 
}

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值