进程控制

进程的创建

  • 第一个进程:进程0,是在操作系统内核的启动过程中手工构成的。
  • 第二个进程:进程1,是由进程0在内核态下通过fork而来。
  • 其他的进程:在用户态下,通过fork而来。
创建进程(fork)
  • 注意:在不同进程中fork返回值不同
    • 在父进程中,fork返回值为子进程PID
    • 在子进程中,fork返回值为0
    • 在fork内部就已经开始分流了,根据判断当前是哪个进程,从而返回不同的值。
  • fork之前的代码只有父进程执行,fork之后的代码由父子进程分别共同执行(但是两个进程谁先被调度是不确定的)
  • 在创建子进程后,操作系统会给子进程创建一个进程地址空间和页表,但是此时子进程的进程地址空间所对应的物理地址和父进程的进程地址空间所对应的物理地址是一样的,即父子进程同时使用一块物理内存(代码和数据共用)
  • 但是,当子进程或父进程对数据进行更改时(例如子进程更改数据),子进程会进行 “写时拷贝” 将整个数据段在物理地址中拷贝一份,并且让进程地址空间对应的虚拟地址映射到拷贝后的物理地址中。这样不会影响父进程的数据(进程具有独立性)
#include< pid_t fork(void); //返回值为进程的PID

fork创建失败的情况
  • 系统中进程太多。
  • 实际用户的进程超过了限制。
进程终止
  • 程序结束,进程正常终止时,会完全释放消耗的系统资源(内存、IO等)
  • 如果没有释放,则相应的资源就会丢失(异常终止时)
  • Linux系统设计时规定:每个进程结束时,操作系统会自动回收这个进程的所有资源(例如malloc申请的内存没有free释放时,进程结束后这个内存也会被释放)
  • 但是操作系统的回收只会回收这个进程工作时消耗的内存和IO,而不会回收这个进程本身占用的内存(主要是task_struck和栈内存)
进程终止方法
  • 正常终止:
    • 程序正常结束,从main函数return返回。
    • 调用 exit() 函数退出进程。( void exit(int status); )
    • _exit ( void _exit(int status); )
  • 异常终止:
    • 程序异常
    • 命令行输入ctrl + c 发送退出信号。
使用 return、exit、_exit 终止进程时的区别
  • 使用return 和 exit 终止函数时,都会释放曾经占用资源(例如行缓冲区等)
  • 使用_exit 终止程序时 ,是直接终止进程,不会释放系统资源。
进程等待
  • 进程等待一般都是由父进程完成。
  • 父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息。
wait方法
wait函数原型
  • wstatus :用来返回子进程结束时的状态信息(传参一个int类型的变量的地址即可)
  • pid_t :这个是返回本次wait回收的子进程的PID。
#include #include pid_t wait(int *status);

wait工作原理
  • 子进程结束时,操作系统会向父进程发送SIGCHILD信号。
  • 父进程调用wait函数时,会等待SIGCHILD信号(函数会阻塞在这里)
  • 父进程收到信号后,会被唤醒然后去回收僵尸进程(子进程退出,但还未被回收时的状态)
  • 如果父进程没有子进程,但是调用了wait函数,这时wait函数会返回错误。
  • 如果父进程有多个子进程,wait函数会阻塞到其中一个子进程结束为止。
  • 所以父进程如果调用wait函数,那么某一个子进程一定会先比父进程结束。
waitpid方法
wait和waitpid差别
  • 基本功能是一样的,都是用来回收子进程。
  • 但是waitpit可以回收指定PID的子进程。
  • waitpit可以选择阻塞式和非阻塞式两种工作模式。
waitpid函数原型
#include #include pid_t waitpid(pid_t pid, int *status, int options);

  • pid :指定要回收的子进程PID(传递值为 -1 时,表示不指定要回收的子进程)
  • status :用来存储子进程结束时的状态信息(传参一个int类型的变量的地址即可)
  • options :实现某些特殊功能。
    • 传递的参数为 0 时 :表示不使用此项(即使用阻塞式)
    • WNOHANG :如果没有子进程,则立马返回 0(非阻塞式)
退出结果status
  • status 是int 类型,32位,实际使用时只使用低16位。
  • status 的第 8 -- 15 位就是退出码。
  • 异常退出时,status的低 7 位是终止信号(终止信号可以用 kill -l 显示)
    进程控制
    文章图片
检测status退出状态的宏
  • WIFEXITED(status) :查看进程是否正常退出,若是则为真。
  • WEXITSTATUS(status) :若进程正常退出,则查看进程退出码。
进程程序替换(exec函数) 替换原理
  • 用fork创建子进程后,父子进程执行的是相同的程序,但是为了让子进程执行不同的程序往往需要调用一种 exec 函数来替换掉子进程中原来的程序。
  • 当调用exec函数后,该进程的物理空间的数据会被新程序完全替换,并且重新建立映射关系,然后让进程从新程序开始执行。
  • 注意:调用exec函数并不创建新进程,所以调用前后进程的PID并未被改变。
  • 所以,有了exec族函数,子进程中的代码可以单独编写、单独编译链接成一个可执行程序。
  • 当调用exec族函数后,则会加载新的程序,从新程序的启动代码开始执行,不再返回当前代码,那么之前进程的后续代码将不会被执行。
    进程控制
    文章图片
替换函数
execl和execv函数
  • path :要执行的可执行程序的全路径。
  • arg :给可执行程序传递的参数。
  • ... :变参,表示可以传递多个参数,但是最终结尾的参数必须是NULL。
  • argv[ ] :参数数组,要事先将传递的参数写到数组中。
  • 如果调用出错,则返回 -1,调用成功则执行新程序,不会返回。
int execl(const char *path, const char *arg, .../* (char*) NULL */); int execv(const char *path, char *const argv[]);

  • 例如:执行 ls 程序,传递参数 -a -l
    • 这里第一个传参ls,貌似没有什么作用,只是占位,第二个开始的参数才会传递到程序中。
execl("/bin/ls", "ls", "-l", "-a",NULL);

execlp和execvp函数
  • 效果与上面的两个函数相同
  • 但是上面两个只能指定可执行程序的全路径(否则会找不到文件而报错)
  • 而这两个函数可以只传递可执行程序的文件名,也可以是全路径
  • 【进程控制】它会在先去当前目录下找file,如果找到了则直接执行,如果没找到则会去环境变量PATH所指定的目录中去寻找,如果还是没找到则报错。
    int execlp(const char *file, const char *arg, .../* (char*) NULL */); int execvp(const char *file, char *const argv[]);

execle和execvpe函数
  • 执行可执行程序时会多传一个环境变量的字符串数组 envp 给待执行程序。
  • 传递的这个环境变量会替代默认的环境变量,在找不到file时,会到这个环境变量中去找,而不会到默认环境变量PATH中去找。
int execle(const char *path, const char *arg, .../*, (char *) NULL, char * const envp[] */); int execvpe(const char *file, char *const argv[],char *const envp[]);

    推荐阅读