UNIX环境编程进程管理

进程管理

概要
  • 进程与程序
    • 进程就是运行中的程序
      • 一个程序可能有多个进程同时进行
      • 进程在操作系统中执行特定的任务
    • 程序(代码)是存储在磁盘上,包含可执行的机器指令的静态实体
      • 进程或者任务是处于活动状态的计算机程序
      • 程序加载到内存中新城内存映像
  • 进程分类
    1. 交互进程(bash终端)
    2. 批处理进程(一直处理任务)】
    3. 守护进程(运行在后台)
      • 创建守护进程的步骤
      • 一般是由系统在开机时通过脚本自动激动启动,或者由超级用户root来启动 (以后如果实现一个服务器,就有可能作为守护进程)
查看

在终端键入以下命令即可查看:

# ps
	(1) 简单形式,以简略方式显示当前用户有控制终端的进程信息
# ps aux   
	(2) 以BSD风格显示
	a - 所有用户有控制终端的进程
	x - 包括无控制终端的进程
	u - 以详尽方式显示
	w - 以列大列宽显示
# ps -elf
	(3) 以SVR4风格显示
	-e / -A - 所有用户的进程
	-a      - 当前终端的进程
	-u uid  - 特定用户的进程
	-g gid	- 特定组的进程
	-f		- 按完整格式显示
	-F		- 按更完整格式显示
	-l		- 按长格式显示
		
# USER/UID 		- 进程属主
# PID			- 进程ID     getpid() 获取当前进行程的ID
# PPID			- 父进程ID
# %CPU/C		- CPU使用率
# %MEM			- 内存使用率
# VSZ			- 占用虚拟内存大小(KB)
# RSS			- 占用物理内存大小(KB)
# TTY			- 终端次设备号    ?表示无终端控制的进程   
# STAT/S		- 进程状态
		O - 就绪,等待被调度
		R - 运行,Linux下没有O状态,就绪状态也用R表示
		S - 可唤醒的睡眠。系统中断,获得资源,收到信号,都可被唤醒,唤醒之后进入运行状态
		D - 不可唤醒的睡眠,只能被wake_up系统调用唤醒
		T - 暂停。收到SIGSTOP信号转入暂停状态,收到SIGCONT信号转入运行状态
		X - 死亡。不可见
		Z - 僵尸进程。已停止运行,但其父进程尚未获取其状态(没有回收资源)
		< - 高优先级
		N - 低优先级
		L - 有被朱丹到内存中的分页。实时进程和定制IO
		s - 会话首进程   setuid
		l - 多线程化的进程
# START/STIME	- 进程开始时间
# TIME			- 进程运行时间
# COMMAND/CMD	- 进程指令/程序名
# F				- 进程标志
		1 - 通过fork产生但是没有exec
		4 - 拥有超级用户特权
# NI			- 进程nice值   -20,19区间取值
# PRI			- 进程优先级  静态优先级  = 80+nice  60,99 值越小优先级越高
# ADDR			- 内核进程的内存地址,普通进程显示 -
# SZ			- 占用虚拟内存页数
# WCHAN			- 进程正在等待的内核函数和事件
# PSR			- 进程被绑定到哪个处理器CPU


# top  linux任务管理器
	动态化的ps
	按k   再输入 pid  可以关闭某个进程
	按q   退出
	
# ps aux | grep 3025/pid/cmd   查看是否有某个进程
层次
  • 父进程
    • 子进程
    • 孤儿进程
    • 僵尸进程
  • 开机启动 内核进程(0) 创建子进程
    • init(1号进程)
      • xinetd
  • 一个进程可以创建另外一个进程,被创建的进程称为子进程,创建进程的进程称为父进程
  • 父进程创建子进程后,子进程在操作系统的调用度与父进程同时运行
  • 子进程先于父进程结束 ,子进程向父进程发送SIGCHLD(17)信号,父进程回收子进程的相关资源
  • 父进程先于子进程结束 ,子进程成为孤儿进程,孤独进程一般会被init进程收养,即成为init进程的子进程
  • 子进程先于父进程结束 ,但父进程没有回收子进程的相关资源 ,该子进程即成为僵尸进程
标识
  • 每个进程都有一个以非负整数表示的唯一标识,即进程ID ,process id
  • 在一个操作系统中,进程ID在任何时刻都是唯一的,唯一标识一个进程
  • 进程ID可以重复使用,当一个进程退出以后,其进程ID就可以分配给新的进程使用(取决于操作系统,一般都是延迟重用)
#include <unistd.h>

pid_t getpid();   //获取当前进程ID
pid_t getppid();  //获取父进程的ID    在终端中运行./a.out  进程的父进程永远都为当前终端进程

uid_t getuid();   //实际用户ID  登录的用户
uid_t geteuid();  //有效用户ID  一般而言会等于实际用户ID   如果一个文件拥有S_ISUID设置用户ID位,有效用户ID等于程序的属主用户ID

gid_t getgid();  //实际组ID
gid_t getegid(); //有效组ID
创建子进程函数:fork()
#include <unistd.h>
//创建子进程
pid_t fork(void);
  • 创建子进程,失败返回-1
  • 如果成功,调用一次,但是返回两个结果
    • 分别在父进程中和子进程中都有返回值
      • 父进程中返回子进程的ID
      • 子进程返回0
    • 所以在编程中,可以用返回值作为条件,分别为父子进程编写不同的代码
  • 写个程序创建10个进程:
#include <stdio.h>
#include <unistd.h>

int main(int argc,const char* argv[])
{
	int i = 0;
	for(i; i < 9; i++){
		pid_t id = fork();
		if(id == -1){
			perror("fork");
			return -1;
		}
		if(id == 0){
			break;
		}
	}
	printf("hello\n");
	//getchar();
	return 0;
}

总结:直接创建会像分裂一样创建210个进程,需要结束多余的子进程

vfork
#include <unistd.h>
int main()
{
    pid_t id = vfork();
    if(id == -1){
        perror("vfork");
        return -1;
    }
    if(id == 0){
        exit(0);//否则产生段错误
    }
}

  • 功能几乎和fork一致,
    • 调用vfork创建子进程时并不会赋值父进程的地址空间
    • vfork之后,确保子进程先执行(被调度)
    • exit退出
  • 如果直接为vfork创建的子进程编写代码,不能用return结束子进程,只能用exit(0)结束,但一般来说不会为子进程编写逻辑代码,会调用exec()系列函数族,直接启动另一个进程替换子进程本身,进而提高进程的创建效率
子进程

子进程是父进程的副本

  • 子进程创建时获得父进程的数据段和堆栈段(包括IO流缓冲区)的拷贝

  • 子进程复制了父进程的内存空间的数据

  • 子进程共享父进程的代码段!!!

  • 在创建子进程时,并不会在创建子进程时立刻复制父进程的内存空间

    • 写时复制(延迟复制) 原因是为了提高创建进程的效率
  • 父子进程数据空间虽然拥有相同的标识符,而且标识符拥有相同的内存地址,实际上是两份独立的

  • 内存地址 ---------- 虚拟内存

    • 不同进程之间的内存地址是毫无意义的
  • 如果在创建进程之前,在父进程的有申请动态内存,则子进程中也会拥有和父进程一样的动态内存(包括动态内存中的数据也是一样的),但是,父子进程中的动态内存是两个独立的动态内存,需要分别释放

  • fork函数调用之后,父子进程各自继续运行(子进程的数据空间数据来源于父进程)

    • 父子进程运行的先后顺序不确定,某些实现(vfork)可以保证子进程先执行
  • 共享文件表

    • fork函数调用后,父进程的文件描述符表(进程级,每个进程独自一份)会拷贝到子进程中,在fork之前打开的文件,fork之后,父子进程中都打开,所以需要各自close,虽然父子进程文件描述表是各自独立的,但fork之前open的文件共享同一个文件表(内核级)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6j8pybLp-1661345891068)(E:%5CTypora’s%20photo%5C%E5%88%9B%E5%BB%BA%E5%AD%90%E8%BF%9B%E7%A8%8B%E4%B9%8B%E5%89%8D%E6%89%93%E5%BC%80%E7%9A%84%E6%96%87%E4%BB%B6.png)]

  • 主进程从main函数开始运行,创建的子进程从fork下面开始执行
孤儿进程
  • 父进程先于子进程结束,子进程变为孤儿进程,一般被init收养,但现代linux都被其他进程收养
僵尸进程
  • 子进程先于父进程结束,向父进程发送SIGCHILD(17)信号,但是父进程没有回收子进程, 此时子进程成为僵尸进程
#include <stdio.h>
#include <unistd.h>

int main()
{
	pid_t id = fork();
    if(id == 1){
        perror("fork");
        return -1;
    }
    if(id == 0){
        printf("child process:%u\n",id);
    }
}
内存映射
int main()
{
	char *p = mmap(NULL, 4096, PORT_READ|PORT_WRITE, MAP_SHARED|MAP_ANONYMOUS,0,0);
    if(p == MAP_FAILURE){
		perror("mmap");
        return -1;
    }
    strcpy(p, "aaaaaaaaa");
    pid_t id = fork();
    if(id == -1){
		perror("fork");
    	return -1;
    }
    
    if(id == 0){
        strcpy(p, "hello");
        sleep(2);
    }
    else{
		sleep(1);
        printf("%s\n", p);
    }
    return 0;
}
/*
结果上父进程输出的p是子进程的hello
*/

mmap是建立虚拟内存和物理内存的映射关系,在fork之后,子进程也会建立虚拟内存和物理内存的映射,父子进程相当于映射到同一块物理内存

  • 创建子进程时,可以执行和父进程相同的代码分支,也可以让子进程执行不同的代码分支
  • 创建进程时,如果资源不够,会创建失败
退出
  1. 从main函数中执行return

  2. 调用标准C语言库函数中的exit(0)函数

    #include <stdlib.h>
    void exit(int status);
    
    • 进程退出的状态或者返回值在父进程中可以获取,获取的只有状态或者返回最低八个二进制位
    • 在终端获取一个进程的状态或者返回值:echo $?
    • 在父进程中,可以通过wait/waitpid获取进程退出状态和返回值
    • exit(0)底层调用了_exit和_Exit
  3. 调用_exit或者_Exit都能正常退出

    #include <unistd.h>
    
    void _exit(int status);  //c标准库中有一个相同功能的函数
    
    #include <stdlib.h>
    void _Exit(int status);
    
  4. 进程的最后一个线程执行了返回语句

  5. 进程的最后一个线程调用了pthread_pid语句

正常退出
  • 先调用事先通过atexit/on_exit函数注册的函数

    #include <stdlib.h>
    void atexit(void (*function)(void));
    int on_exit(void (*function)(int,void *),void *arg);
    
    
    
    
  • 功能,用于注册函数,在该进程结束之前调用
    如果注册了多个函数,则进程结束之前的调用顺序和注册的顺序正好相反

    on_exit 返回0表示注册成功 返回非0表示注册失败
    atexit注册的函数没有返回值,且没有参数
    on_exit注册时,除了需要给定注册的函数,还需要给定第二个参数arg,这个arg用于在调用注册函数时作为第function的第二个参数,function的第一个参数是进程退出状态或者返回值

  • 冲刷并关闭所有任然处于打开状态的标注IO流

  • 删除所有通过tmpfile创建的临时文件

#include <stdio.h>
FILE *tmpfile(void);

  • 建议使用EXIT_SUCCESS/EXIT_FAILURE常量宏来替换进程的退出状态和返回值提高平台的兼容性
异常退出
  • 调用abort函数,产生SIGABRT信号

  • 进程接收到了某些信号 段错误(由信号产生的)

    • 可以给某个进程发送信号 kill -sig pid
        #kill -9 18922    #给18922进程发送9信号
        sig [1,64]   pid 指定的进程
        
        #kill -l          #信号列表
  • 最后一个线程对pthead_cancel请求做出响应
wait/waitpid
#include <sys/types.h>
#include <sys/wait.h>

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

等待子进程终止将获取其终止状态,成功返回终止进程的进程号(进程ID),失败返回-1

wait

  • wait 函数,用于等待子进程结束,如果没有子进程结束,则调用此函数会阻塞,直到有子进程结束为止
  • 如果已经有子进程结束了,然后父进程调用wait会直接获取该结束的子进程的状态并返回子进程ID
  • 如果多个子进程结束,则会选择任意一个子进程返回ID

  • 如果没有子进程可等待了,则返回-1, 且errno设置为ECHILD
  • wait函数参数可以为NULL, 表示不获取子进程的退出状态
  • 在任何一个子进程结束前,调用wait函数只能阻塞调用进程,而waitpid有更多的选项

waitpid

pid取值
   <-1  	- 等待其组ID等于该参数绝对值的任一子进程,即等待属于特定进程组内的任一子进程
	-1		- 等待任意一个子进程,此时效果等同于wait函数
 	>0		- 等待由pid参数所标识的唯一的特定的进程
 	 0		- 等待其组ID等于调用进程组ID的任一子进程,即等待与调用进程同属于一个进程组内的任意子进程
 	
options:
	0				- 阻塞模式    等同于wait
	WNOHANG 		- 非阻塞模式,若没有合适的结束的子进程,则返回0  只有在非阻塞时才会返回0
	WUNTRACED		- 若支持,且子进程处于暂停状态,则返回其状态     暂停   STOP
	WCONTINUED		- 若支持,且子进程暂停后继续运行,则返回其状态
	
status:  作为输出参数,用于获取进程的状态标识   包含1.进程是否正常退出  2.异常退出的信号  3.返回值
	若取NULL,则表示不关心进程的状态
	WIFEXITED(status) - 用于判断子进程是否是正常终止 如果正常退出则为真
		如果子进程正常退出,则可以用 WEXITSTATUS(status)宏获取子进程return或者exit/_exit/_Exit函数所返回值的低8个二进制位
	WIFSINGALED(status) - 用于判断子进程是否是异常终止,如果是则为真
		如果子进程异常退出,则可以用 WTERMSIG(status)宏获取终止子进程的信号
    WIFSTOPPED(status)  - 若支持,可以判断子进程是否处于暂停状态
    WIFCONTINUED(status) - 若支持,可以判断子进程是否在暂停之后继续运行
int main()
{
    int i;
    pid_t id[10];
    for(int i = 0; i< 10; i++){
        id[i] = fork();
        if(id[i] == -1){
            perror("fork");
            return -1;
        }
        if(id[i] == 0){
            printf("child process:%u\n", getpid());
            sleep(i*i);
            return i*i;
        }
    }
    printf("father :%u, creat 10 child success!\n", getpid);
    
    int s;
    pid_t pid;// = waitpid(-1, &s, 0);
    int opt = 0;
    while(1){
        printf("1. wait pid\n");
        printf("2. try wait pid\n");
        printf("3. wait any pid\n");
        printf("4. try wait any pid\n");
        int in;
        scanf("%d",&in);
        if(in == 1 || in == 3){
            opt = 0;
        }
        else if(in == 2 || in == 4){
            opt = WNOHANG;
        }
        if(in == 1 || in == 2){
            printf("input pid:");
            scanf("%u",&pid);
        }
        else{
			pid = -1;
        }
        
        pid_t ret = waitpid(pid, &s, opt);
        if(ret == -1){
            if(errno == ECHILD){
                printf("all over\n");
                break;
            }
            perror("waitpid");
        }
        else{
            printf("%u process over\n", getpid());
            if(WIFEXITED(s)){
                
            }
            else if(WIFEXITED(s)){
                
            }
        }
    }
}
exec系列函数
#include <unistd.h>

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[]);

int execve(const char *filename,char *const argv[],char *const envp[]);
  • e的需要传递环境列表

    • 以数组形式传递环境列表,且其最后一个元素为NULL
  • v的说明参数列表是以数组的形式传递,数组的最后一个元素为NULL

  • l的说明参数列表必须通过可变长参数列表传递 参数列表必须以NULL为结尾

  • p的若file中不包含"/",则将其视为文件名,根据PATH环境变量进行搜索该文件

    • 没有p时,可执行文件需要带路径,否则只会从当前目录下查找
  • exec函数会用新进程完全代替调用进程,并开始从main函数(exec系列函数第一个参数的main函数)执行

  • exec函数并非创建子进程,新进程取调用进程的PID

  • exec函数所创建的新进程,完全取代调用进程的代码段,数据段和堆栈段

  • exec函数若执行成功,不会返回,失败返回-1

  • exec函数基本上用于vfork创建的子进程

    • vfork创建的子进程不会拷贝父进程的地址空间
system
#include <stdlib.h>

int system(const char *command);
  • 标准C语言函数,执行command命令,成功返回command对应进程的终止状态,失败返回-1
  • 测试shell是否可用 ,当command取NULL时,返回非0表示shell可用,返回0表示shell不可用
  • 该函数的实现fork+exec+waitpid
    • 如果调用fork或waitpid出错,返回-1
    • 如果调用exec函数出错,则在子进程中执行exit(127)
    • 如果都成功,则返回command对应进程的状态由waitpid获取
  • system函数对各种错误和信号都做了必要的处理

信号

kill -9 pid
    
  • 子进程结束后会给父进程发送SIGCHILD(17)信号
  • ctrl+c
  • ctrl+\
概念

是一种软件中断

  • 中断
    • 中止正在执行的程序,转而执行其他任务
      • 其他任务结束后可继续执行
    • 软件中断
      - 程序中断
    • 硬件中断
      - 来自硬件的中断

信号提供了一种异步执行任务的机制

  • 列出信号 kill -l
信号解释产生条件默认动作
SIGINT(2)终端中断符信号ctrl+c终止
SIGQUIT(3)终端退出符信号ctrl+\终止+core文件
SIGABRT(6)异常终止信号abort()终止+core文件
SIGBUS(7)总线错误信号硬件故障(内存故障)终止+core文件
SIGKILL(9)终止信号不能被捕获和忽略,用于杀死进程终止
SIGCHLD(17)子进程改变状态信号子进程终止时,向父进程发送忽略
SIGTSTP(20)终端停止信号ctrl+z,发送给前台进程组中所有进程停止进程
# jobs   可以查看当前终端后台任务
# fg [jobs编号]  可以把后台执行任务调到前台执行
# cmd  &   后台执行   不占用终端   终端就可以继续交互
分类
  • 可靠信号
    • 位于SIGRTMIN[34,64]的信号称为可靠信号
    • 支持排队,不会丢失,同一信号产生几次就能接受几次
  • 不可靠信号
    • 建立在早期机制上的信号称为不可靠信号,小于SIGTMIN(34)的信号都是不可靠信号
    • 不支持排队,可能会丢失,同一个信号产生多次但系统只收到一次
    • 进程每次处理完这些信号,对相应信号的响应被自动恢复为默认处理(现在都不会)
来源
  • 硬件异常,除0、对0取余、无效内存访问
    • 这些异常会被硬件设备检测到,并通知系统内核
    • 系统内核向引发异常的进程递送相应的信号
  • 软件异常
    • kill/alarm/sigqueue/settimer函数都会产生信号
处理
  1. 默认处理
    1. 忽略 SIG_IGN
    2. 终止进程
    3. 终止进程并产生core文件(堆栈信息)
  2. 捕获并处理
    1. 在信号发生之前注册信号处理函数,当信号发生时就会调用实现注测好的信号处理函数
    2. 某些信号不能被捕获和处理,不能为用SIG_IGN忽略这些信号,也不能为这些信号注册信号处理函数
    3. 在某些UNIX系统上,通过signal注册的信号处理函数只有一次有效,即调用信号处理函数后,就恢复成默认的处理方式
#include <signal.h>

typedef void (*sighandler_t)(int);

sighandler_t signal(int signum,sighandler_t handler);
/*
	给signum信号事先注册信号处理函数handler
	
	返回值:
		失败返回  SIG_ERR
		成功返回  信号之前的处理函数
	
	当成功注册一个信号的处理函数时,程序会继续往下执行,但是当程序接收到该信号时,程序会中断正在执行的代码,转而执行注册的信号处理函数handler,执行完handler之后程序继续回到中断的位置继续执行
	
	参数:
		signum  - 要给哪个信号注册信号信处函数   可以使用整数[1,64]   可以使用宏 SIGINT   SIGQUIT
			SIGSTOP   SIGCONT
		handler - 信号处理函数指针
			SIG_IGN	表示直接忽略该信号
			SIG_DFL 使用默认处理方式
		某些信号不能被捕获和处理,所以不能为这些信号注册信号处理函数,如 SIGKILL  9
		
	在某些unix系统上,通过signal注册的信号处理函数只有一次有效,即调用完信号处理函数之后,就恢复成默认的处理方式,为了获得持久有效信号处理,需要在信号处理函数中再次调用singal函数进行重新注册
*/


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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值