UnixC( 四)之进程管理

1.1 进程基础
进程是资源分配的基本单位(因为进程执行的时候,独占CPU),进程也是程序执行过程中,对使用到的资源的描述。每个进程都有自己的pid,还有自己的PCB,PCB记录了进程中资源的使用情况。

  • 数据类型
  • 运算符的优先级及其结合性
  • 常量和变量
  • 变量的作用域和生命周期

1.1.1 进程中常用命令

查看进程树

  • pstree

察看进程的信息

  • ps -aux
  • S(sleep)
  • Z(zombie)
    查看任务:
  • top命令

1.1.2 创建子进程

fork(void);
功能:创建一个子进程
参数:void
返回值:
失败 在父进程里返回-1 子进程没有被创建 errno被设置
成功 父进程里返回子进程的pid
子进程里返回0

  • 问题: 父子进程谁先执行。
    (1)顺序确定就是同步
    (2)顺序不定就是异步

  • 示例代码

#include <stdio.h>
#include <unistd.h>
int main()
{
	int pid ;
	printf("before fork ...\n");   //这句话只在父进程中执行

	pid =fork(); //fork以后的代码才会在父子进程中都执行。
	if( -1 == pid)
	{
		perror("fork");
		return -1;
	}
	if(0 == pid)
	{
		printf("child  process\n");

	}
	else
	{
		printf("parent process\n");
	}
	printf("end fork ...\n");
	return 0;
}

1.1.3 fork 调用过程
在这里插入图片描述

  • 父子进程有各自的进程4G空间(堆栈,数据段,代码段),在各自的进程空间中对值改变,父子之间相互不影响。
  • 每个进程都有自己的代码段,数据段,堆栈段,和进程控制块(PCB),PCB是操作系统感知进程的一个重要的数据结构。进程是操作系统对资源的一种抽象。
  • fork是系统调用,程序一般运行在用户态,当执行到fork的时候,程序会陷入到内核态,当fork返回之前,子进程已经创建了,所以父子进程在各自的空间返回。

1.1.4 思考问题

  • 问题一 :fork的时候,为什么子进程是从fork的位置执行,而不是从main函数重新开始执行?
    原因 1: 如果从main函数执行,那么程序就会无休止的一直创建进程
    原因2:父进程在fork之前进行内存四区的创建,子进程直接继承就可以,没有必要重新创建一次。

1.1.5概念理解

  • 孤儿进程 : 父进程死掉,子进程还存在,子进程的父进程变成init进程(1号进程)
  • 进程的退出
    (1) return 是从函数中返回。在main函数中意味着进程的结束,普通函数中return 并不意味着进程的结束
    (2)在普通函数中调用exit 也意味着进程的结束。
  • exit(3)
exit -  cause normal process termination
 All open stdio(3) streams are flushed and closed.
 - 无返回值,
 - The exit() function causes normal process termination and the value of status & 0377 is returned to the parent  (see wait(2)).

exit(-1) 返回给父进程的值是多少 255

1.1.5.2 进程退出的5种方式

  • return
  • exit
  • _exit
  • abort
  • 发送一个信号,比如 ctrl +c

1.1.5.3 exit 和 _exit 的区别
exit 是 库函数 (man 3 exit ) , 而 _exit 是系统调用 ,exit 在进程退出前会调用遗言函数,并且刷新缓冲区域。

int main()
{
	printf("hello");  //注意这里没有 \n
	_exit(1);
}

1.1.6 遗言函数

  • 遗言函数
    遗言函数 进程在死之前要处理一些事请,调用函数执行这些事情,这样的函数称为遗言函数。
    在进程死之前要注册遗言函数,在进程死了的时候执行遗言函数

  • atexit();
    注意点:
    1、调用的顺序和注册的顺序相反
    2、同一个函数被注册一次就被调用一次
    3、子进程继承父进程的遗言函数

  • on_exit(3)

 #include <stdlib.h>
 int on_exit(void (*function)(int , void *), void *arg);
The  on_exit()  function  registers the given function to be called at normal process termination, whether via
exit(3) or via return from the program's main().  The function is passed the status argument given to the last call to exit(3) and the arg argument from on_exit().
 The same function may be registered multiple times: it is called once for each registration.
When  a  child  process is created via fork(2), it inherits copies of its parent's registrations.  Upon a successful call to one of the exec(3) functions, all registrations are removed.
typedef struct Teacher
{
	char name[64];
	int age;
}Teacher;
//遗言函数
void doit(int n,void *arg){
	
	Teacher * tmp = (Teacher *)arg;
	printf("n=%d\targ=%s\n",tmp->age,tmp->name);
    return;
}
int main(void){

	static	Teacher teacher;    //注意这是static 局部变量,如果这不是具有全局生命周期的变量,调用注册的遗言		函数就会乱码
	strcpy(teacher.name ,"zhangsan");
	teacher.age = 10;
//	doit(1,&teacher);
    //向进程注册遗言函数
    on_exit(doit, &teacher);
 //   getchar();
    return -1;
}

1.7 进程状态
1.7.1进程状态
在这里插入图片描述
1.7.2 进程状态
Linux内核中的进程状态

  • 运行状态(TASK_RUNNING)
  • 可中断睡眠状态(TASK_INTERRUPTIBLE)
  • 不可中断睡眠状态(TASK_UNINTERRUPTIBLE)
  • 暂停状态(TASK_STOPPED)
  • 僵死状态(TASK_ZOMBIE)
    在这里插入图片描述
    1.7.3 举例
  • 可中断睡眠和不可中断睡眠
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
void func(int sig)
{
	printf("sig =%d \n",sig);
}
int main()
{
	int n;
	signal(SIGINT,func);
	do
	{
		n =	sleep(100);  // 这里是不可中断睡眠,可以查看sleep的文档说明,尤其是返回值。
	}while(n >0);
	return 0;
}

1.2 进程资源的回收

(1) 进程在执行期间占用一定的资源,在进程终止的时候需要将资源释放给系统,有父进程负责回收子进程的资源。
系统提供了wait(2)家族的函数用于回收进程的资源

1.2.1 僵尸进程

  • 僵尸进程是子进程终止了,父进程还没有回收子进程占用的资源。
  • 举例
int main(void){
    //创建子进程
    pid_t pid=fork();
    if(pid==-1){
        perror("fork");
        return -1;
    }
    if(pid==0){//子进程执行的代码
        printf("child pid:%d\n",getpid());
        exit(0);
    }else{//父进程执行的代码
        getchar();
    }
    return 0;
}

1.2.2 wait系统调用

  • 子进程终止,父进程还没有回收资源,这时候子进程照样存在。

函数原型:pid_t wait(int *status);

DESCRIPTION
       All  of  these  system  calls are used to wait for state changes in a child of the calling process, and obtaininformation about the child whose state has changed.  A state change is considered to  be:  the  child  terminated;  the  child was stopped by a signal; or the child was resumed by a signal.  In the case of a terminated child, performing a wait allows the system to release the resources associated with the child; if  a  wait  is  not performed, then the terminated child remains in a "zombie" state

代码实例一:

int main()
{

	int pid;
	pid = fork();
	if( -1 == pid)
	{
		perror("fork");
		return -1;
	}
	if(0 == pid)
	{
		printf("child pid :%d\n",getpid());
		//getchar();
		exit(0);
	}
	else
	{
		getchar();
		wait(NULL);     //wait 至关重要
		printf("parent pid :%d\n",getpid());
		getchar();
	}
	return 0;
}

代码示例二 :
补充:
如何给指定的进程发送信号?
kill -信号编号 pid
给进程发信号是在还运行的时候发的,如果进程都死了(比如 return ,exit等), 还发信号就没有意义了

#include <sys/wait.h>
int main()
{
	int pid;
	int status;
	pid = fork();
	if( -1 == pid)
	{
		perror("fork");
		return -1;
	}
	if(0 == pid)
	{
		printf("child pid :%d\n",getpid());
		getchar();
		exit(-1);		
	}
	else
	{
		wait(&status); //等待子进程终止,将子进程的退出状态存储在 status中
		//检测子进程是否是正常终止
		if(WIFEXITED(status))
		{	
			printf("child  status %d\n",WEXITSTATUS(status));
		}
		//检查是否子进程是否被信号打断
		if(WIFSIGNALED(status))
		{
			printf("signal num : %d\n",WTERMSIG(status));

		}
		printf("parent pid :%d\n",getpid());
	}

运行结果:
第一种情况 给进程发送2 号信号
在这里插入图片描述
第二种情况: 直接ctrl + c;
第三种情况: 进程正常终止。
在这里插入图片描述
1.2.3 waitpid使用
pid_t waitpid(pid_t pid, int *status, int options);
功能:
参数:
pid:可以指定具体的一个pid
pid的取值
<-1 pid的绝对值是要等待的子进程的组id
-1 等待任意子进程
0 等待子进程,这些被等待的子进程的组id和父进程的组id必须一致
> 0 等待指定的进程的pid

status:同wait(2)的参数功能一样
options:
WNOHANG:如果没有子进程终止,立即返回。
0 阻塞
返回值:
成功 返回被终止的子进程的pid
如果WNOHANG被指定,没有子进程终止,返回0
错误 -1

进程组:进程组中有多个进程

wait(&status)==waitpid(-1, &status, 0);

1.2.4 waitpid 使用实例

  • 实例1 创建多个子进程,并将多个子进程变成僵尸进程
int main()
{
	int pid;
//	signal(SIGCHLD,func);
	for(int i =0 ;i<5;i++)
	{
	
		pid = fork();
		if( -1 == pid)
		{  
			perror("fork");
			return -1;
		}
		if(0 == pid)
		{
			printf("child pid :%d\n",getpid());
			//getchar();
			exit(0);		
		}
		else
		{
				waitpid(-1,NULL,WNOHANG);   // 这里不阻塞。
		}
	}
	while(1)
	{
			sleep(1);
	}
	return 0;
}
  • waitpid 和 wait 一块使用
int main()
{

	int pid;
	int status;
	for(int i =0 ;i<5;i++)
	{
	
		pid = fork();
		if( -1 == pid)
		{
			perror("fork");
			return -1;
		}
		if(0 == pid)
		{
			printf("child pid :%d\n",getpid());
			//getchar();
			exit(10);		
		}
		else
		{
				waitpid(-1,NULL,WNOHANG);
		}
	}
	sleep(20);
	wait(&status);
	if(WIFEXITED(status))
	{
		printf("status = %d\n",WEXITSTATUS(status));
	}
	if(WIFSIGNALED(status))
	{
		printf("signal = %d\n",WTERMSIG(status));
	}
	sleep(20);
	printf("main end\n");
	return 0;
}
  • 重中之重,wait函数会一直阻塞,直到子进程发送一个信号过来。上面的程序中,在第一个sleep没有运行完之前是有五个僵尸进程的,但是当sleep完成之后,wait收到一个信号,会回收子进程,这时候只剩下四个僵尸进程。

1.3 更换进程映像
1.3.1 概念

  • CPU,文件,内存都是资源,进程是资源分配的基本单位。
  • fork 复制进程的PCB,每个进程都有自己的4G 地址空间
  • 进程的映像也是PCB的一部分,文件描述符也是PCB的一部分。
  • 加载映像并不影响打开文件的描述符。
  • 继承以后,父子进程共享已经打开的文件描述符,如果子进程再打开的话,就和父进程就没有关系了。(已经验证)
    1.3.2 execve函数解析

#include <unistd.h>
int execve(const char *filename, char *const argv[], char *const envp[]);

返回值 : 成功不返回,失败返回-1, errno被设置。

       execve() does not return on success, and the text, data, bss, and stack of the calling process are overwritten  by that of the program loaded.
       execve()函数成功不返回,将进程的代码段,数据段,堆栈段进行了替换(也就是替换了进程的映像)
       

功能:执行程序
参数:
filename:指定了要执行的程序的文件名
argv:字符串列表,按照惯例,第一个字符串是filename
envp:通过这个参数给进程传递环境变量。key=value

返回值:
执行成功不返回
执行失败返回-1,errno被设置

注意:自定义变量是进程私有的,不能被子进程继承,而环境变量是可以被子进程继承的,envp参数默认是从父进程继承环境变量

1.3.3 封装了execve 的库函数
除了这个函数之外,系统封装这个函数提供了很多的库函数用于替换进程的映像。这些函数都属于execl(3)家族
#include <unistd.h>
extern char **environ;
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[]);

l:argv[0] argv[1]…NULL
v:vector argv
p:指环境变量PATH。如果函数的名字里有这个字母。之需要指定可执行文件的名字即可。可执行程序会到PATH环境变量指定的路径下找这个文件。
如果函数的名字里没有这个p字母,需要指定可执行文件的路径。
e:环境变量。函数的名字中没有这个字母,从父进程默认继承环境变量。如果有这个字母,使用指定的环境变量覆盖从父进程继承而来的环境变量。(暂时没有举例)

重要:  ps -o pid,ppid,pgrp,session,comm

程序示例1:


int main()
{

	int pid;
	char * p_argv[]= {"ps","-o","pid,ppid,pgrp,session,comm",NULL};
	pid = fork();
	if( -1 == pid)
	{
		perror("fork");
		return -1;
	}
	if(0 == pid)
	{
	//更改进程的映像。
//		execvp("ps",p_argv);
		execv("/bin/ps",p_argv);
		//execv错误的时候才会运行到下面的程序,execv正确的时候下面的程序不运行
		perror("execv");
		exit(-1);
	}
	else
	{
		wait(NULL);
	}
	return 0;
}

1.3.4实例程序2 ,不调用fork,只是单单的替换a.out 的映像。

int main()
{
execlp("ps","ps", "-o" ,"pid,ppid,pgrp,session,comm",NULL);
return 0;
}

exec家族的函数是用来替换进程的映像。
执行a.out的时候发生了什么?
bash首先通过fork(2)创建子进程,然后execl(2)更换进程的映像为a.out。

bash上执行的是linux命令,这些命令分为内部命令和外部命令

外部命令都是独立的可执行程序,bash也是一个独立的可执行程序。和bash不是同一个命令,这些命令都是外部命令
内部命令就是bash的一部分程序而已。bash执行这些程序的时候,不用fork。
外部命令的实现,需要bash fork子进程,然后用exec家族的函数替换进程的映像。

内部命令的实现,暂时没有验证(可能是这样,先记下来)
比如: cd 命令
func( pathname)
{
	//改变路径
}
在主函数中 对  "cd   test " 进行分割,分割以后把 test 路径传递给func函数。

如何察看一个命令是内部命令还是外部命令?
type command
在这里插入图片描述
重中之重:

int main()
{
execlp("ps","ps", "-o" ,"pid,ppid,pgrp,session,comm",NULL);
return 0;
}
  • bash进程 先fork再exec a.out ,在a.out中,没有fork(即没有生成子进程),而是直接将a.out的映像替换成了ps程序
  • bash 内部命令,相当于在bash进程中调用了一个函数而已
  • bash 外部命令: 先fork ,然后再exec外部命令。

补充说明
a.out 程序就是通过 fork,然后exec,最后执行的。

参考一 : UNIX 环境高级编程
参考二 : 网上视频

参考一 :狄泰软件学院C++进阶剖析
参考二 : C++ primer中文版
如有侵权:请联系邮箱 1986005934@qq.com

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值