#Linux中的GCC编程# 进程相关的知识

基于C,进一步研究 linux内核函数,系统级别的函数

2017年8月

1. 知识了解

1.1 基本概念:

(1)程序 program
被存放在磁盘中的可执行文件。
(2)进程 process
是程序的执行实例。每个进程具有独立的权限和职责,运行在各自的虚拟地址空间中。
进程之间不会相互影响,但可以进行通信。
(3)进程ID,process ID简称PID
非负整数,进程的数字标识符。
进程任务队列

1.2 启动例程

(1)“启动例程”被编译在main之前运行。
(2)收集命令行的参数,传递给main中的argc,argv。以及环境表envp。
(3)登记“终止函数”atexit()。

1.3 进程的终止

(1)正常终止

  • 从main函数返回 (return)
  • 调用exit(标准C库函数)
  • 调用_exit或_Exit(系统调用)
  • 最后一个线程从“启动例程”返回
  • 最后一个线程调用pthread_exit

注意:
return和exit(),会刷新标准IO缓存,会自动调用终止函数。
_exit()和_Exit(),不刷新,也不调用。

(2)异常终止

  • 调用abort
  • 接收一个信号并终止
  • 最后一个线程对“取消”请求 做出响应

(3)进程返回

  • 通常程序成功返回0,否则返回非0
  • 在shell中,可以产看进程返回值(echo $?)

(4)终止函数

#include <stdio.h>
int atexit(void (*function)(void));
向内核登记终止函数,成功返回0,否则-1
  • 每个启动的进程都默认登记一个标准的终止函数
  • 终止函数在进程终止时释放进程所占用的一些资源
  • 登记多个终止函数,执行顺序以栈的方式执行,先登记后执行。

1.4 示意图

启动示意图

1.5 进程资源限制

linux中可用的资源resource如下:

RLIMIT_AS 			进程可用的存储区大小
RLIMIT_CORE 		core文件最大字节数
RLIMIT_CPU 			CPU时间最大值
RLIMIT_DATA			数据段最大长度
RLIMIT_FSIZE		可创建文件的最大长度
RLIMIT_LOCKS		文件锁的最大数
RLIMIT_MEMLOCK使用mlock能否在存储器中锁定的最长字节数
RLIMIT_NOFILE		能打开的最大文件数
RLIMIT_NPROC		每个用户ID可拥有的最大子进程数
RLIMIT_RSS			最大驻内存集的字节长度
RLIMIT_STACK		栈的最大长度
  1. 头文件
#include <sys/resource.h>

struct rlimit{
	rlim_t rlim_cur;/*软件限制:当前限制*/
	rlim_t rlim_max;/*硬件限制:当前限制可以达到的最大值*/
}
  1. 函数
(1)获取进程的资源限制,存放在rlptr指向 的结构体中。成功返回0,失败非0。
int getrlimit(int resource , struct rlimit *rlptr);

(2)修改resource指定的资源限制,通过rlptr指向的结构体。成功返回0
int setlimit(int resource,const struct rlimie *rlptr);
  1. 配置文件
    (1)/etc/security/limits.conf
    (2)linux中,进程资源的初始化由0号进程建立,并被后续进程继承。
  2. 资源限制的修改规则
    (1)硬件资源限制必须大于等于软件限制。
    (2)任何一进程可以降低或者提升其软件资源限制,但必须大于其软件限制。普通用户不可逆此操作。
    (3)超级用户可以提高硬件限制。

2. 与进程有关的指令

2.1 PS指令

可以查看到:进程ID(PID),进程的用户ID,进程状态STAT,进程的command等等。

3. 进程常见状态

3.1 运行状态

系统当前的进程
就绪状态进程
PS命令的stat列 == R

3.2 等待状态

等待事件发生
等待系统资源
PS命令的stat列 == S

3.3 停止状态

PS命令的stat列 == T

3.4 僵尸状态

进程终止或结束
在进程表项中仍有记录
PS命令的stat列 == Z

3.5 进程状态的变换关系

进程状态变换关系

4. 进程的调度

4.1 一般性步骤

(1)处理内核中的工作
(2)处理当前进程
(3)选择进程(实时进程和普通进程)
(4)进程交换

4.2 task_struct中的调度信息

(1)策略

  • 轮流策略
  • 先进先出策略

(2)优先权

  • jiffies变量

(3)实时优先权

  • 实时进程之间

(4)计数器

5. 进程标识

进程有很多的标识:
当前进程ID,实际用户ID,有效用户ID,用户组ID,父进程ID,进程组ID。
与此相关的函数:

  1. 头文件
#include <unistd.h>
#include <sys/types.h>
  1. 获取进程标识的函数
pid_t getpid(void);           //获取当前进程的ID标识
pid_t getppid(void);         //获取父进程的ID标识
pid_t getpgrp(void);        //获取当前进程所在的进程组ID标识。
pid_t getpgid(pid_t pid);  //获取指定ID的进程所在的进程组ID标识。

uid_t getuid(void);           //获取当前进程的 实际用户ID
uid_t geteuid(void);          //获取当前进程的 有效用户ID

gid_t getgid(void);           //获取当前进程的用户组ID

6. 进程的创建

本文的重点内容。

6.1 创建子进程的函数 fork

  1. fork函数
    fork创建的新进程被称为子进程。该函数被调用一次,会返回两次。
    返回两次的区别:
    (1)在父进程里,返回的是 新子进程的进程ID。
    (2)在新子进程里,返回的是0。因为子进程的数据段、堆、栈都是重新创建的。
    (3)父和子进程的运行顺序,根据系统调度自动决定。
    (4)子进程复制父进程的内存空间。

  2. vfork函数
    与fork类似,但是 子进程先行运行,且不复制父进程的内存空间。

  3. 子进程的继承属性

  • 用户信息和权限
  • 目录信息,信号信息,环境,资源限制
  • 共享存储段,堆,栈和数据段,共享代码段
  1. 子进程的特有属性
  • 进程ID
  • 锁信息
  • 运行时间
  • 未决信号
  1. 操作文件时内核结构变化
  • 子进程继承文件描述表,不继承但共享文件表项和i-node。
  • 创建一个子进程后,文件表项中的引用计数器加1变成2,当父进程作close操作后,计数器减1。子进程还是可以使用文件表项。只有当计数器为0时,才会释放文件表项。

6.2 进程寄生 exec函数簇

  • exec函数用于执行另一个程序。新执行的程序会替换原进程的正文,数据,堆,栈。
  • exec并不是创建新进程,前后的进程ID并没有改变。
  • 在fork创建一个子进程之后,可以在子进程中使用exec函数执行另一个程序。
  1. 头文件
#include <unistd.h>

2.函数

//list 列出每个字符参数
int execl(const char *pathname,const char *arg0, ... /*(char*)0*/);
//argv 字符数组
int execv(const char *pathname,char * const argv[]);
//list 列出每个字符参数,环境表
int execle(const char *pathname,const char *arg0, ... /*(char*)0,char* const envp[]*/);
//argv 字符数组,环境表
int execve(const char *pathname,char * const argv[],char* const envp[]);
//
int execlp(const char *pathname,const char *arg0, ... /*(char*)0*/);
//
int execvp(const char *pathname,char * const argv[]);

上述所有的返回:出错返回-1 ,成功不返回。

6.3 system函数

  • system函数,内部构件一个子进程,由子进程调用exec函数。
  1. 头文件
#include <stdlib.h>

2.函数

简化exec函数的使用,成功返回执行命令的状态,错误返回-1
int system(const char * command);

7. 几种特殊的进程

7.1 守护进程 daemon

  • 守护进程是生存期很长的一种进程。他们常常在系统引导装入时启动,在系统关闭时终止。
  • 所有守护进程都是以超级用户(用户ID为0)的优先权运行。
  • 守护进程没有控制终端。
  • 守护进程的父进程都是init进程。

7.2 孤儿进程

  • 父进程结束,子进程就成了孤儿进程。由1号进程(init进程)领养。

7.3 僵尸进程

  • 子进程结束,但是没有完全释放内存(在内核中的task_struct没有释放)。该进程就成了僵尸进程。
  • 当僵尸进程的父进程结束,由1号进程(init进程)领养,最终回收。
  • 如何避免僵尸进程:
    (1)让僵尸进程的父进程来回收。父进程每隔一段时间来查询子进程是否结束并回收。调用wait()或者waitpid(),通知内核释放僵尸进程。
    (2)采用信号SIGCHLD,通知处理。并在信号处理程序中调用wait()。
    (3)让僵尸进程成为孤儿进程,由init进程来回收。
  1. 头文件
#include <sys/types.h>
#include <sys/wait.h>
  1. 函数
(1)等待子进程退出并回收。防止僵尸进程。成功返回子进程ID,出错返回-1。
pid_t wait(int *status);

(2)wait函数的非阻塞版本。成功返回子进程ID,出错返回-1。
pid_t waitpid(pid_t pid , int * status ,int options);

	1)pid参数。
			pid==-1   //等待任一子进程,功能和wait等效。
			pid== 0   //等待 同进程组ID的任一子进程。
			pid >  0   //等待指定的子进程
			pid < -1   //等待 组ID等于(-pid)的任一子进程。
	2)status参数。为空时,等待回收【任意状态】结束的子进程。不为空,则等待【指定状态】结束的子进程。
         检查wait和waitpid函数返回终止状态的宏
				WIFEXITED/WEXITSTATUS(status)    //若为正常终止子进程返回的状态,则为真。
				WIFSIGNALED/WTERMSIG(status)    //若为异常终止子进程返回的状态,则为真。(接到一个不能捕捉的信号)
				WIFSTOPED/WSTOPSIG(status)     //若为当前暂停子进程返回的状态,则为真。
	3)options参数。
				WNOHANG    //若由pid指定的子进程没有退出,则立即返回。waitpid不阻塞,返回值为0。
				WUNTRACED    //若某实现支持 作业控制,则有pid指定的任一子进程状态已暂停,其状态自暂停以来还没有报告过,则返回其状态。

  1. wait和waitpid函数的区别
    (1)在一个子进程终止前,wait使其调用者阻塞。waitpid 不会阻塞。
    (2)wait等待所有的子进程;waitpid等待指定的子进程。

8. 进程组

  • 一个或者多个进程的集合
  • 可以接收同一终端的各种信号。同组共信号。
  • 唯一的进程组ID
  • 组内所有的进程结束,进程组才会消亡。
  • kill命令 发送信号给进程组。

8.1 获取进程组号

前文已经给出:

#include <unistd.h>
pid_t getpgrp(void);        //获取当前进程所在的进程组ID标识。
pid_t getpgid(pid_t pid);  //获取指定ID的进程所在的进程组ID标识。

8.2 组长进程

  • 进程组的创建从第一个进程(组长进程)加入开始。组长进程的ID作为组ID。
  • 组长进程可以创建进程组以及该组中的进程。
  1. 头文件
#include <unistd.h>
  1. 函数
(1) 将进程加入到指定的进程组中。成功返回0,错误返回-1。
int setpgid(pid_t pid,pid_t pgid);

(2) pid参数为指定的进程号,pgid为进程组。

8.3 前台进程组

  • 在linux中,自动接收终端信号的组,成为前台进程组。
  • 在终端中,CTRL+c 之类的信号,首先被前台进程组接收。
  • shell启动的若干个进程组,默认父进程所在的组为前台进程组。
  1. 头文件
#include <unistd.h>
  1. 函数
(1)获取前台进程组ID。成功返回 前台进程组ID,错误返回-1。
pid_t tcgetpgrp(int fd); 

(2)使用pgrpid,设置前台进程组ID
int tcsetpgrp(int fd,pid_t pgrpid);

(3)fd必须引用该会话的控制终端。0表示当前正在使用的终端。

猜你喜欢

转载自blog.csdn.net/Kshine2017/article/details/102624873