【Linux】系统编程——进程基础知识/创建/终止/等待

目录

基础概念

程序和进程

进程的状态

如何创建一个进程

进程控制编程

获取ID

进程创建 fork()

vfork()  (比较少使用)

exec函数族

execl ()

execlp ()

execv ()

system ()

进程终止 exit()  _exit()

 exit()

 _exit()

孤儿进程

僵尸进程

守护进程

进程等待  wait()  waitpid()

wait() 

waitpid()


基础概念

搜索文件“15、系统编程第二天”

增加:

程序和进程

程序运行起来,产生一个进程

程序,是指编译好的二进制文件,在磁盘上,不占用系统资源(cpu、内存、打开的文件、设备、锁....)

进程,是系统资源分配的最小单位。是一个抽象的概念,进程是活跃的程序,占用系统资源。在内存中执行。

进程的状态

  • 初始态:一般与就绪态归为一类
  • 就绪态:一切都准备好了,就等待CPU分配时间片了
  • 运行态(执行态):占用CPU中
  • 挂起态:等待除了CPU以外的其他资源,主动放弃CPU(遇到缺少资源或发生中断)
  • 终止态

为什么要有三态?

多个进程一起进入执行态可能会发生多进程死锁,动不了了。三态的好处就是有一个缓冲区,可以一个一个去处理它

如何创建一个进程

运行一个可执行文件,fork,vfork,exec函数族,system(clone是啥?)

并发与并行

并发执行:就是CPU轮换的执行,当前进程执行了一个短暂的时间片(ms)后,切换执行另一个进程,如此循环往复,由于时间片很短,
在宏观上我们会感觉到所有的进程都是在同时运行的,但是在微观上cpu每次只执行某一个进程的指令。(单核CPU)

并行执行:如果cpu是多核的话,不同的cpu核可以同时独立的执行不同的进程,这种叫并行运行。所以当cpu是多核时,并发与并行是同时存在的。

进程互斥

进程互斥是指当有若干进程都要使用某一共享资源时,任何时候最多允许一个进程使用,其他要使用该资源的进程必须等待,直到占用该资源者释放了该资源为止。

进程同步

一组并发进程按一定的顺序执行的过程称为进程间的同步。

进程同步包含(保证)进程互斥。

就像上厕所,如果没有同步,没有访问顺序,一个人出来,其他人谁先抢到谁用,有了同步,厕所外可似乎排队,一个一个按顺序用。
具有同步关系的一组并发进程称为合作进程,合作进程间互相发送的信号称为消息或事件。

临界资源&临界区

  • 临界资源:操作系统中将一次只允许一个进程访问的资源称为临界资源。
  • 临界区:进程中访问临界资源的那段程序代码称为临界区。为实现对临界资源的互斥访问,应保证诸进程互斥的进入各自的临界区。

为什么要保持进程同步——进程死锁

多个进程因竞争资源而形成一种僵局,若无外力作用,这些进程都将永远不能再向前推进。

进程调度

操作系统的核心就是任务(进程)管理

  • 先来先服务调度算法
  • 最短作业优先调度
  • 基于优先级调度
  • 循环调度或时间片轮转法

linux进程特点

Linux系统是一个多进程的系统,它的进程之间具有并行性、互不干扰等特点。也就是说,每个进程都是一个独立的运行单位,拥有各自的权利和责任。其中,各个进程都运行在独立的虚拟地址空间,因此,即使一个进程发生异常,它也不会影响到系统中的其他进程。

每个进程拥有独立进程空间的优缺点

优点:

  • 对编程人员来说,系统更容易捕获随意的内存读取和写入操作
  • 对用户来说,操作系统将变得更加健壮,因为一个应用程序无法破坏另一个进程或操作系统的运行(防止被攻击)

缺点:

  • 多任务实现开销较大
  • 编写能够与其他进程进行通信,或者能够对其他进程进行操作的应用程序将要困难得多

进程控制编程

获取ID

#include <sys/types.h>
#include <unistd.h>

pid_t getpid(void);      //返回当前进程的id
pid_t getppid(void);    //返回父进程的id

例.

#include <sys/types.h>
#include <unistd.h>

int main()
{
    pid_t pid;

    pid = getpid();
    printf("pid = %d\n",pid);

    while(1);

    return 0;
}

进程创建 fork()

#include <sys/types.h>
#include <unistd.h>

pid_t fork(void);

/*
  返回值:
      1)正值(新创子进程的进程ID):父进程;
      2)0:子进程:;
      3)负值:出现错误;
*/
  • 当fork()顺利完成任务时,就会存在两个进程,每个进程都从fork()返回处开始继续执行。         
  • 两个进程执行相同的代码(text)段,但是有各自的堆栈(stack)段、数据(data)段以及堆(heap)。
  • 子进程的stack、data、heap segments是从父进程拷贝过来的。(读时共享写时复制)
  • fork()之后,哪一个进程先执行不确定。如果需要确保特定的执行顺序,需要采用某种同步(synchronization)技术(semaphores,file locks...)。      
  • 父子进程共享:文件描述符,mmap建立的映射区(两个进程间建立一个映射区,完成进程值之间数据传递)
  • 父子进程相同之处:全局变量(是数据值相同,不是共享!)、.data、.text、栈、堆、环境变量、用户ID、宿主目录、进程工作目录、信号处理方式
  • 父子进程不同之处:1.进程ID   2.fork返回值   3.父进程ID    4.进程运行时间    5.闹钟(定时器)   6.未决信号集

读时共享,写时复制

表面看起来fork()创建子进程子进程拷贝了父进程的地址空间(早期系统的确是这样的)其实不然 刚调用完fork()之后,子进程只是拥有一份和父进程相同的页表,其中页表中指向RAM代码段的部分是不会改变的,而指向数据段,堆段,栈段的会在我们将要改变父子进程各自的这部分内容时,才会将要操作的部分进行部分复制

shell并不知道运行的进程创建了子进程,所以shell进程在进程结束之后就开始执行自己,如果此时子进程并为结束运行,shell与子进程共同抢占CPU,所以会出现以下情况

 

 例.

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>

int main()
{
	pid_t pid;
	fork();
	fork();
	pid = fork();

	if(pid < 0)
	{
		perror("fork");
		return -1;
	}
	else if(pid > 0)
	{
		printf("parent pid is %d\n",getpid());
		while(1);
	}
	else if(0 == pid)
	{
		printf("child pid is %d\n",getpid());
		while(1);
	}   
	return 0;
}

3个fork()调用后,共有8个进程(1生2,2生4,4生8) 

 


vfork()  (比较少使用)

#include <sys/types.h>
#include <unistd.h>

pid_t vfork(void);
  • 子进程共享父进程的代码,数据,堆栈资源(父进程与子进程共享空间,变量可互相使用,改了一个变量之后另一个进程变量也改变了。)
  • 使用vfork后,直接运行exec,节省了资源拷贝的时间
  • 使用vfork,创建子进程后直接运行子进程,父进程被阻塞,直到子进程执行了exec()或者exit()。
  • 子进程退出使用return会破坏父进程的堆栈环境(会释放数据段),产生段错误,所以退出使用exit或_exit

目的

vfork是为子进程立即执行exec的程序而专门设计的:

  • 无需为子进程复制虚拟内存页或页表,子进程直接共享父进程的资源,直到其成功执行exec或是调用exit退出。
  • 在子进程调用exec之前,将暂停执行父进程

 


clone()

linux 进程创建clone、fork与vfork

fork()是全部复制,vfork()是共享内存,而clone()是则可以将父进程资源有选择地复制给子进程,

而没有复制的数据结构则通过指针的复制让子进程共享,具体要复制哪些资源给子进程,由参数列表中的clone_flags来决定。
另外,clone()返回的是子进程的pid。

 


exec函数族

无成功返回值,成功不返回,之后的程序不执行,失败返回-1,并执行之后的程序。一般exec之后只跟perror与exit两句就行了(也不用判断了直接写就行)

exec函数族和fork的区别

fork创建子进程后执行的是和父进程相同的程序。而子进程可以调用exec函数从而能执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。

exec函数名中英文字母意义(方便记忆)

  • l (list)                         命令行参数列表(命令写在".........","........","......",NULL)
  • p (path)                     在用户的绝对路径path下查找可执行文件,该文件必须在用户路径下,可以只指定程序文件名
  • v (vector)                  使用命令行参数数组(命令全都写在数组中,只传数组进去)
  • e (environment)        为新进程提供新的环境变量

事实上,只有execve是真正的系统调用,其它五个函数最终都调用execve

一般来说

  • execlp:系统可执行程序
  • execl:用户自定义可执行程序

一些可能会用到的命令

  • whereis 查看命令的路径
  • pwd 查看文件的路径

execl ()

#include <unistd.h>
 
int execl(const char * path, const char* arg1,...)


/*
  参数:
      path : 被执行程序名(含完整路径);
      arg1 - argn: 被执行程序所需的命令行参数,含程序名。以空指针(NULL)结束.
*/

例.

#include <stdio.h>
#include <unistd.h>
#include <errno.h>

int main()
{
	int ret;

#if 0
	ret = execl("/bin/ls","ls","-a","/home",NULL); //第一个参数是ls的绝对路径,-a表示显示隐藏文件,/home表示列举home路径下的文件
	if(ret < 0)
	{
		perror("execl");
		return -1;
	}
#endif

	ret = execl("/mnt/hgfs/share/2019/0119/exe5_8","exe5_8",NULL);  //执行该绝对路径下的文件
	if(ret < 0)
	{
		perror("execl");
		return -1;
	}
}

 注:execl执行成功后自行结束了程序,所以execl函数之后的它都不会去执行。execl会载入你调用的程序,覆盖原有代码段,相当于你本来的程序的代码段被替换成execl执行的了,所以execl后面的都不会输出了。正确使用方法应该是fork一个子进程,在子进程中调用execl。

execlp ()

#include <unistd.h>
 
int execlp(const char * path, const char* arg1,...)


/*
  参数:
      path : 被执行程序名(不含路径,将从path环境变量中查找该程序;
      arg1 - argn: 被执行程序所需的命令行参数,含程序名。以空指针(NULL)结束.
*/

例.

#include <stdio.h>
#include <unistd.h>
#include <errno.h>

int main()
{
	int ret;

#if 0
	ret = execlp("ls","ls","-a","/home",NULL); //第一个参数是ls的相对路径,-a表示显示隐藏文件,/home表示列举home路径下的文件
	if(ret < 0)
	{
		perror("execlp");
		return -1;
	}
#endif

#if 1
	ret = execlp("../0119/exe5_8","exe5_8",NULL);  //执行该相对路径下的文件
	if(ret < 0)
	{
		perror("execlp");
		return -1;
	}
#endif

}

 

execv ()

#include <unistd.h>
 
int execv(const char * path, const char *argv[])

/*
  参数:
      path :  被执行程序名(含完整路径);
      argv[]: 被执行程序所需的命令行参数数组。
*/

 例.

#include <stdio.h>
#include <unistd.h>
#include <errno.h>

int main()
{
	int ret;

#if 0
	char *argv[] = {"ls","-a","/home",NULL};
	ret = execv("/bin/ls",argv); 
	if(ret < 0)
	{
		perror("execv");
		return -1;
	}
#endif

#if 1
	char *argv1[] = {"exe5_8",NULL};
	ret = execv("/mnt/hgfs/share/2019/0119/exe5_8",argv1);  
	if(ret < 0)
	{
		perror("execv");
		return -1;
	}
#endif

}

system ()

#include <stdlib.h>
 
int system(const char* string)

/*
  函数说明:
      创建子进程,并加载新程序到子进程空间,运行起来。      
      调用fork产生子进程,由子进程来调用 /bin/sh -c string来执行参数string所代表的命令。命令行怎 
      么输,string里面就怎么写
*/

例.

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>

int main()
{
	int ret;

	system("../0119/exe5_8");

	return 0;

}

 

进程终止 exit()  _exit() —— 一个刷缓冲区一个不刷

 exit()

#include<stdlib.h>

void exit(int status);

/*
  函数说明:
      exit()用来正常终结目前进程的执行,并把参数status返回给父进程。
  参数:
      用于标识进程的退出状态,shell或父进程可获取该值
      0:表示进程正常退出
      -1/1:表示进程退出异常
      2-n:用户可自定义
*/

 

 

 

 _exit()

#include<unistd.h>

void _exit(int status);

/*
  函数说明:
      此函数调用后不会返回,并且会传递SIGCHLD信号给父进程,父进程可以由wait函数取得子进程结束状 
      态。
*/

 

 正常退出

  • main调用return
  • 任意地方调用exit库函数
  • 任意地方调用_exit函数

异常退出

  • 被信号杀死
  • 调用abort函数

孤儿进程

父进程先于子进程结束,则子进程成为孤儿进程,子进程的父进程成为init进程,称为init进程领养孤儿进程。init进程的id可能为1也有其他。孤儿进程最终肯定都由init进程回收。

其执行顺序大致如下:在一个进程终止时,内核逐个检查所有活动进程,以判断它是否是正要终止的进程的子进程,如果是,则该进程的父进程ID就更改为1(init进程的ID);

 

僵尸进程

进程终止,父进程尚未回收(获得终止子进程的有关信息,释放它仍占用的资),子进程残留资源(PCB)存放于内核中,变成僵尸(Zombie)进程

残留的PCB是为了让父进程知道子进程的死亡状态,如果意外死亡可能需要报仇。如果一个进程在其终止的时候,自己就回收所有分配给它的资源,系统就不会产生所谓的僵尸进程了。如果不好好回收进程,这些残留的PCB会占用很多内存直至溢出。

产生过程(看看就行):

  1. 父进程调用fork创建子进程后,子进程运行直至其终止,它立即从内存中移除,但进程描述符仍然保留在内存中(进程描述符占有极少的内存空间)。
  2. 子进程的状态变成EXIT_ZOMBIE,并且向父进程发送SIGCHLD 信号,父进程此时应该调用 wait() 系统调用来获取子进程的退出状态以及其它的信息。在 wait 调用之后,僵尸进程就完全从内存中移除。
  3. 因此一个僵尸存在于其终止到父进程调用 wait 等函数这个时间的间隙,一般很快就消失,但如果编程不合理,父进程从不调用 wait 等系统调用来收集僵尸进程,那么这些进程会一直存在内存中。

 怎么回收僵尸进程

除了wait和waitpid函数,用户用kill命令其实回收不了,因为它本身已经死了。还有一个办法就是杀死父进程,这样僵尸进程变为孤儿进程,被init领养,最后都由init回收。
有init领养的进程不会称为僵死进程,因为只要init的子进程终止,init就会调用一个wait函数取得其终止状态。这样也就防止了在系统中有很多僵死进程。

守护进程

守护进程(daemon)详解与创建:下面代码具体的函数信息以及为什么要这么做都在这里面

linux之创建守护进程(稍微简单一点,但还是推荐详细的)

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdlib.h>

int main()
{
	pid_t pid;
	int i;

	pid = fork();
	if(pid < 0)
		return -1;
	else if(pid > 0)
		exit(0);

	setsid();//创建一个会话,把它变成该会话的组长

	pid = fork();
	if(pid < 0)
		return -1;
	else if(pid > 0)
		exit(0);

	chdir("/");		//改变文件目录
	umask(0);		//文件掩码

	for(i = 0; i < getdtablesize(); i++)	//关掉所有文件描述符
	{
		close(i);
	}

	while(1)
	{
		system("echo test >> /test.log");		//每隔5秒往文件里写一个test(echo没有>>是向屏幕打印的意思,有>>是重定向,向文件里写入内容)
		sleep(5);
	}
	
	return 0;
}

该函数实现了一种后台进程:每隔5秒相test.log里写入一个英文单词test

echo:相当于输出。echo 1>1的意思是向文件1输入1

ps =ef | grep a.out:显示进程,用以验证a.out的确是在运行中的

ps aux:显示所有正在运行的进程及其具体信息?

tail -f | test.log:定时刷新显示test.log中的内容,以验证的确是每隔5秒被写入一个test

kill -9 5344:杀死进程id为5344的进程,告诉它终止的信号是编号为9的信号

kill -l:查看信号以及对应编号

 

进程等待  wait()  waitpid()

wait() 

#include <sys/wait.h>

pid_t wait(int *status);

/*
  返回值:
      若成功返回回收的子进程ID,若出错返回-1(也就是没有子进程可以回收了)。
*/

功能

  • 阻塞等待子进程退出(子进程不退出,父进程就等待,不执行其他程序)
  • 回收子进程残留资源
  • 获取子进程结束状态(退出原因)(若不想知道原因,直接wait(NULL)即可)

有4个互斥的宏可以用来获取进程终止的原因:

  • WIFEXITED(status) —— Wait IF EXITED

    若子进程正常终止,该宏返回true。
    此时,可以通过WEXITSTATUS(status)获取子进程的退出状态(exit函数的参数或者return的参数,此宏返回一个int)。

  • WIFSIGNALED(status)

    若子进程由信号杀死,该宏返回true。所有进程异常退出的根本原因是收到了信号。
    此时,可以通过WTERMSIG(status)获取使子进程终止的信号值。

  • WIFSTOPPED(status)

    若子进程被信号暂停(stopped),该宏返回true。
    此时,可以通过WSTOPSIG(status)获取使子进程暂停的信号值。

  • WIFCONTINUED(status)

    若子进程通过SIGCONT恢复,该宏返回true。 

如果一个子进程已经终止,并且是一个僵死进程,wait立即返回并取得该子进程的状态,否则wait使其调用者阻塞直到一个子进程终止。如果有多个子进程,只有一个wait,wait只能回收最先终止的进程。

while(wait(NULL));         //可以将子进程全部回收完,再做下面的工作

例.

#include <stdio.h>
#include <stdlib.h>		//exit函数要用到
#include <sys/wait.h>
#include <unistd.h>

int main()
{
	pid_t pid, wpid;
	int i, status;

	pid = fork();

	if(pid < 0)
	{
		perror("fork error:");
		exit(1);
	}
	else if(pid == 0)
	{
		printf("child:%d,parent:%d\n",getpid(),getppid());
		sleep(3);
		exit(78);
	}
	else
	{
		for(i = 0; i < 10; i++)
		{
			printf("----parent:%d----\n",getpid());
		}

		wpid = wait(&status);

		if(wpid == -1)
		{
			perror("wait error:");
			return 76;
		}
		if(WIFEXITED(status))
		{
			printf("child exit with %d\n",WEXITSTATUS(status));
		}
		if(WIFSIGNALED(status))
		{
			printf("child kill by %d\n",WTERMSIG(status));
		}
	}

	for(i = 0; i < 10; i++)
	{
		printf("-----after wait------\n");
	}
}

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <errno.h>
#include <sys/wait.h>

int main()
{
	pid_t pid;
	pid = fork();
	int status;

	if(pid < 0)
		return -1;
	else if(0 == pid)
	{
		printf("child pid is %d\n",getpid());
		//while(1);
		_exit(0);
	}
	else
	{
		printf("parent pid is %d\n",getpid());
		wait(&status);
		printf("WIFEXITED %d",WIFEXITED(status));
	}
	return 0;
}

/*
   程序结果:
       while(1)的话,status那个不会输出,应为被阻塞了
       _exit(0)的输出为1
*/

waitpid()

#include <sys/wait.h>

pid_t waitpid (pid_t pid, int * status, int options)

/*
  函数功能:
       作用同wait,但可指定pid进程清理,可以不阻塞。
 
  参数:
       *status:如果不在意结束状态值,则参数status可以设成NULL。

       pid:欲等待的子进程识别码:
            pid> 0 回收指定ID的子进程
            pid=-1 等待任何子进程,相当于wait()。
            pid<-1 回收指定进程组为pid绝对值内的任意子进程
            pid=0 回收和当前调用waitpid一个组的所有子进程
           
       option:可以为 0 或下面的 OR 组合
            0:跟wait一样,子进程没结束就一直阻塞
            WNOHANG:  如果没有任何已经结束的子进程则马上返回,不予以等待。
                此时返回值为0表示有子进程正在运行
            WUNTRACED :如果子进程进入暂停执行情况则马上返回,但结束状态不予以理会。

   返回值:
       如果执行成功则返回回收的子进程ID,如果有错误发生则返回-1(也就是没有子进程可以回收了)。失败原因存于errno中。
*/

注意:一次wait或waitpid调用只能清理一个子进程,清理多个子进程应使用循环。

waitpid的不阻塞是指运行到此函数如果有进程还没结束,它能继续执行下面的程序,但不代表它还能等这个进程执行完再返回这个函数进行回收。所以不管wait还是waitpid要是想清理所有进程都得使用循环。

 


进程调度

操作系统的核心就是任务(进程)管理

进程调度器

将有限的CPU资源分配给多个进程

目的:最大化处理器效率,让多个进程同时运行,互不影响

调度机制分类:

  • 协同式(非抢占性/时间片轮转):谁先创建谁先执行,按顺序执行。一个进程运行完自己的时间片,主动退出,CPU无权访问。实时性不够。当一个进程出现异常,产生中断,或者需要做紧急的事情的时候,不能优先执行,要等到自己的时间片到来才能。
  • 抢占式:实时性好。时间片到了或右更高优先级、调度器抢占CPU进行任务切换。能及时相应一些异常和突发状况。每个进程有优先级,先执行优先级更高的。

linux之前是协同式,之后协同式和抢占式共同工作。

调度器把进程分为两类:

  • 处理器消耗型:渴望获取更多的CPU时间,并消耗掉调度器分配的全部时间片。如while死循环,科学计算,影视渲染(很消耗CPU资源)
  • I/O消耗型:由于等待某种资源,通常处于阻塞状态,不需要较长的时间片。如父进程做输入的时候,不做输入的时候就会把时间片让出去

调度器发现你是I/O消耗型,就用优先级调度你,你要用的时候把你的优先级调高,处理器消耗型就用协同式,不让别人打断你,让你把自己的时间片消耗掉,再把使用权让给别人。

 

 

 

 

 

 

 

 

 


面试小结

谈谈你对进程的理解

进程是什么,进程如何创建,创建的方法有哪些,进程如何退出,退出的方法,区别。创建过程中产生的问题僵尸孤儿进程,如何解决。多个进程同时运行需要对进程做调度,哪两类调度,调度策略。多个进程之间传输数据做进程通信

进程是操作系统中分配资源的最小单位。

每个进程都有自己独立的虚拟内存空间,能达到互不干扰,并发并行运行

创建进程的方法fork,vfork,exec,system,各种的特点

进程的退出又分为正常退出和异常退出,如何让进程正常退出?

进程有可能产生僵尸进程和孤儿进程,分别产生的原因?引出init进程

解决僵尸进程,通过进程等待,用wait,waitpid等待

—————————————————————————

进程调度分为抢占式和协同式,抢占式的目的是根据优先级使操作系统的策略更具实时性;协同式根据时间片轮转,一个一个分配对等的时间片让它去执行

进程间通信

为什么需要进程间通信?进程有独立的地址空间,每个进程没有交集,没有交集就不能通信。所有进程有个最大的交集的操作系统,也就是内核空间,所以通过内核空间进行.......

 

 

 

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值