1. 1. 程序和进程
    1. 1.1. 单道和多道
    2. 1.2. 时间片
    3. 1.3. 并行和并发
    4. 1.4. 进程控制块(PCB)
    5. 1.5. 进程状态转换
    6. 1.6. 进程创建
      1. 1.6.1. 原理
      2. 1.6.2. gdb调试
    7. 1.7. exec函数族
    8. 1.8. 进程控制
      1. 1.8.1. 孤儿进程
      2. 1.8.2. 僵尸进程
      3. 1.8.3. wait函数
    9. 1.9. 进程间通信(IPC)
      1. 1.9.1. 匿名管道
      2. 1.9.2. 有名管道
      3. 1.9.3. 内存映射
      4. 1.9.4. 匿名映射
      5. 1.9.5. 信号
      6. 1.9.6. 共享内存
        1. 1.9.6.1. 使用方法
        2. 1.9.6.2. 相关系统操作命令
        3. 1.9.6.3. 共享内存和内存映射的区别
        4. 1.9.6.4. 生命周期
    10. 1.10. 守护进程

进程的创建、控制、进程间通信等

程序和进程

进程是正在运行的程序的实例,是操作系统动态执行的基本单元
可以使用一个程序创建多个进程

单道和多道

单道技术:计算机内存中只允许一个程序运行
多道技术:为提高cpu利用率,允许同时在计算机内存中存放几道相互独立的程序,使他们在管理程序的控制下,相互穿插运行,共享系统资源

时间片

操作系统分配给每一个正在进行的进程微观上的一段cpu时间

并行和并发

并行(Parallel):同一时刻有多条指令在多个处理器上同时进行
并发(Concurrent):同一时刻只能由一条指令执行,但是多个进程指令被快速的轮换交替执行,使其看起来使多个进程同时执行

进程控制块(PCB)

内核为进程所做的事情进行清楚地描述,linux内核的进程控制块使task_srtuct结构体(位于/usr/src/linux-headers-xxx/include/linux/sched.h)
主要包含:进程id,进程状态,进程切换时需要保存或回复的cpu寄存器,描述虚拟地址空间的信息,描述控制终端的信息,当前工作目录,umask掩码,文件描述符表,信号量,用户id组id,会话和进程组,进程资源上限等

进程状态转换

三态模型:就绪态,运行态,稳定态
五态模型:新建态,[三态模型],终止态
就绪态:进程具备运行条件,等待分配处理器,系统会形成一个就绪队列
运行态:进程占用处理器正在运行
阻塞态:又叫等待态/睡眠态,进程不具备运行条件或等待某一事件完成
如何查看进程状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
ps aux / ajx
a:显示终端上所有的进程
u:显示进程的详细信息
x:显示没有控制终端的进程
j:列出与作业控制相关的信息

stat 查看进程信息

top 监控进程资源 -d(更新时间间隔,单位:s)
按下按键对结果排序
m 内存使用
p cpu占用
t 运行时间
u 用户名
k 指定pid杀死进程

kill 杀死进程
-l 列出所有信号
-sigkill 进程id
-9 进程id
killall name 根据进程名杀死进程

进程创建

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include<sys/types.h>
#include<unistd.h>
pid_t fork(void);
/**
为当前进程创建子进程(复制当前进程)
返回值:
成功:子进程中返回0,父进程中返回子进程ID
失败:返回-1,并设置errno
可能的失败原因:
1. 进程数量达到上限,此时errno=EAGAIN
2. 系统内存不足,此时errno=ENOMEM
*/

//获取进程号的方法
pid_t getpid();//获取子进程id
pid_t getppid();//获取父进程id
/**如果父进程终止但子进程没有中止,子进程会被系统领养
*/

原理

父子进程均独立拥有PCB,拷贝后内存块内用户区内容一致,内核区会有所区别
实际上:fork是通过读时共享,写时拷贝实现的,资源在fork后并不立即拷贝,此时为只读共享状态,只有在写入时才会复制地址空间
但值得注意的是:刚被创建出,两个进程均未执行任何写操作时,父子进程共享文件表,相同的文件描述符指向相同的文件表,引用计数增加

gdb调试

gdb默认只跟踪一个进程,可以在调用前设置跟踪某一进程

1
2
set follow-fork-mode [parent|child]
set detach-on-fork [on|off]

默认为on,表示调试当前进程时其他进程继续运行
info inferiors 查看当前调试的所有进程
inferior [id] 切换当前调试的进程
注意:需要通过info inferiors查看id,切换时需要输入的是id,不是pid
detach inferiors [id] 使进程脱离gdb调试独立运行

exec函数族

作用:根据指定的文件名找到可执行文件,然后替换调用进程中的内容,一般会创建子进程,然后在子进程中执行exec,子进程中所有的代码都会被替换,即便exec的内容结束,也并不会执行之后的任何代码

参数 指代 作用
l list 参数地址列表
v vector 存有参数地址的指针数组的地址
p path 按path环境变量指定的目录搜索可执行文件
e environment 存有环境变量字符串地址的指针数组的地址
最初的exec版本:
1
int execve(const char *filename,char *const argc[],char *const envp[]);

其他的所有exec族都是对execve的封装

进程控制

1
2
3
4
5
6
7
8
9
#include<stdlib.h>
void exit(int status);
//位于c语言库中的exit函数
//调用推出处理函数->刷新IO缓冲,关闭文件描述符,之后调用_exit()

#include<unistd.h>
void _exit(int status);
//linux系统的exit函数
//系统级清理资源,进程终止运行

孤儿进程

定义:父进程终止运行,但仍在运行的子进程
孤儿进程的附近成为init,init会循环wait()它的已经退出的子进程,当孤儿进程结束时,init会代为处理善后(调用_wait())
注意:在部分发行版中,孤儿进程并不一定被init收养,但等效

僵尸进程

进程结束之后,会释放用户区数据,但是PCB是由父进程释放的,如果进程终止时父进程没有回收PCB,则会造成占用进程号的僵尸进程出现
僵尸进程不可以被kill -9杀死

解决方案:让父进程死亡,让init接手该进程,会自动回收

wait函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include<sys/types.h>
#include<sys/wait.h>
pid_t wait(int *wstatus);
/**wait函数是阻塞的,等待任一子进程结束后回收其资源
传入:一个int类型的地址,通过该地址传出进程退出时的状态信息
返回值:成功则返回被回收的子进程id,失败返回-1
注意:调用wait后进程会阻塞,直到它的一个子进程退出或者收到一个不能被忽略的信号才能被唤醒
如果没有子进程,会立刻返回-1
如果子进程都结束了,同样会立即返回-1
*/
pid_t waitpid(pid_t pid, int *wstatus, int options);
/**释放指定pid的进程(不阻塞),一次只回收一个
pid:
若pid>0 则为需要被回收的的子进程pid
若pid==0 回收任一进程组的所有子进程(概念:进程组)
若pid==-1 回收任一子进程,不限于进程组(最常用)
若pid<-1 回收某个进程组的组id(取绝对值)
options:
0:阻塞
WNOHANG:非阻塞
返回值:
>0 返回子进程id
=0 options=WNOHANG时表示还有子进程存活
=-1 错误/无任何子进程
*/

进程间通信(IPC)

目的:数据传输、通知事件、资源共享、进程控制等
linux中进程通信的方式(共7种):

同一主机进程间通信

  • Unix进程间通信方式
    • 匿名管道
    • 有名管道
    • 信号
  • System V进程间通信方式/POSIX进程间通信方式
    • 消息队列
    • 共享内存
    • 信号量

不同主机间进程通信方式

  • Socket套接字

匿名管道

匿名管道是UNIX系统IPC的最古老形式
管道的特点:

  • 是一个在内核内存中维护的缓冲器,存储能力有限,在不同操作系统中大小不一定相同
  • 可以读写,匿名管道没有文件实体,有名管道由文件实体但不存储数据,可以按照操作文件的形式操作管道
  • 一个管道是一个字节流,不存在消息或边界的概念,可以读取任意大小的数据块,不管写入的数据块是多少
  • 通过管道传递的顺序是顺序的,读取和写出的顺序完全一致
  • 数据传输是单向的,一端可以写入,一端可以读出
  • 管道读取数据的操作是一次性的,一旦读取便会被遗弃
  • 匿名管道只能在具有公共祖先的进程间使用

管道的数据结构:循环队列
但是,如果管道已满仍然写,write会阻塞

1
2
3
4
5
6
7
8
#include<unistd.h>
int pipe(int pipefd[2]);
/**创建匿名管道
参数:返回两个文件描述符,[0]指向读端,[1]指向写端
返回值:成功返回0,失败返回-1
由于匿名管道只能存在于具有关系的进程之间,文件描述符通过fork时父子进程共享文件描述符表实现的
读取方式和普通的文件没有差别,使用read函数
*/

匿名管道的读写特点
使用管道时需要注意的情况(假设都是阻塞I/O操作):

  • 所有的指向管道写端的文件描述符都关闭时,即写端的引用计数为0,如果有进程从管道的读端读取数据,那么管道中所有的数据被读取以后,再次read会返回0,效果同读到文件末尾
  • 上一种情况,如果有写端没有关闭,即写端的引用计数不为0,read会阻塞
  • 如果读端的引用计数为0,如果有进程写数据,该进程会收到一个SIGPIPE的信号,通常会导致进程异常终止
  • 如果有指向管道读端的文件描述符没有关闭,即管道的读端引用计数大于0,而持有管道读端的进程也没有从管道中读数据,此时有进程向管道中写数据,那么管道被写满时write会被阻塞,直到管道中有空位置时并成功写入时才可以返回

有名管道

相对于匿名管道:

  • 提供了一个路径名与之关联,所以这个管道可以跨亲缘关系通信
  • 在文件系统中作为一个特殊的文件存在,但内容存放在内存中
  • 使用FIFO的进程推出之后,FIFO文件会继续保存在文件系统中
  • 不相关的进程可以通过名字打开FIFO进行通信

有名管道的创建可以通过shell完成

1
mkfifo [name]

也可以通过函数

1
2
3
4
5
6
#include<sys/types.h>
#include<sys/stat.h>
int mkfifo(cosnt char *pathname, mode_t mode)
/**

*/

有名管道的读写特点

  • 访问一端时,若另一端的引用计数为0,该进程会阻塞
  • 读取时,若管道中无数据,写端全部关闭,等效于读到文件末尾
  • 读取时,若管道中无数据,写端尚未全部关闭,进程会阻塞
  • 写入时,若读端被全部关闭,会受到SIGPIPE信号
  • 写入时,若管道已满,会阻塞

内存映射

将文件映射到内存(进程的虚拟地址空间)中,对内存的修改操作会同步操作到文件中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include<sys/mman.h>
void *mmap(void *addr, size_t length, int prot, int flags,int fd, off_t offset);
/**建立映射
参数:
*addr 内存首地址,未知,传递NULL,由内核指定
length 需要映射的文件长度,不可以为0,建议使用文件的长度(stat/lseek)
porot 对申请的内存区的操作权限
PROT_[EXEC/READ/WRITE/NONE] 多个权限用'|'连接
如果相对映射区进行操作,必须要有读权限
flags
MAP_SHARED 映射区会自动和磁盘文件同步
MAP_PRIVATE 映射区改变不会修改源文件,会重新创建文件
fd 需要映射的文件的文件描述符
offset 映射时的偏移量,一般为0,须指定为4K的整数倍,0表示不偏移
返回值:创建的内存首地址
失败返回MAP_FAILED*(void*(-1))


*/
int munmap(void *addr, size_t length);
/** 释放内存映射
参数:
addr 要释放的内存地址
length 要释放的长度,与mmap中的length相等
记得需要释放文件控制符(fd)奥
*/

使用内存映射实现进程间通信:

  • 父进程创建映射区后,创建子进程,此时父子进程共享内存映射区
  • 无关联的进程,通过同一个磁盘文件创造内存映射区即可

匿名映射

不需要文件实体的内存映射
限制:只能存在于父子进程之间

1
2
3
void *mmap(NULL,1024, PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANONYMOUS,-1,0);
/** MAP_ANONYMOUS即为匿名映射的flag
*/

信号

又被称为软件中断/硬中断,是一种异步通信的方式,可以由一个进程直接中断另一个进程,通常源自于内核

目的

  • 让进程知道已经发生了特定的事件
  • 强迫进程执行代码中的信号处理程序

特点

  • 简单
  • 不能携带大量信息
  • 满足特定条件发送
  • 优先级高

使用kill -l查看系统定义的信号列表
前31个信号为常规信号,其余为实时信号

信号的处理

  • 查看信号的详细信息

    1
    man 7 signal
  • 默认处理方式

    • Term 终止
    • Ign 忽略信号
    • Core 终止,生成Core文件(核心转储文件)
    • Stop 暂停当前进程
    • Cont 继续执行当前被暂停的进程
  • 信号的状态

    • 产生
    • 未决
    • 送达
  • SIGKILL和SIGSTOP不可以被捕捉、阻塞或是被忽略,只能执行默认动作

信号相关的函数

1
2
3
4
5
6
7
8
9
10
11
int kill(pid_t pid, int sig)
/**给pid进程发送信号sig
参数:
pid:
>0 指定的进程
==0 发送给当前进程所在的进程组
==-1 发送给每一个有权限接受这个信号的进程
<-1 pid=某个进程组的id(去掉负号)
sig:
需要发送的信号的编号或是宏值,推荐使用宏值,同一宏值在不同系统下表示不同结果
*/
1
2
3
4
int raise(int sig);
/**给当前进程发送信号
返回值:成功返回0,失败返回非0
*/
1
2
void abort(void);
// 给自己发送SIGABORT信号
1
2
3
4
5
6
7
8
9
10
11
unsigned int alarm(unsigned int seconds);
/** 设置定时器,传入倒计时时间,当倒计时为0的时候,函数为向当前进程发送SIGALARM
参数:seconds,单位秒,若<=0,alarm无效
如果想撤销计时器,设置alarm(0)
返回值:
倒计时剩余的时间(当且仅当之前存在计时器)
由于一个进程只有一个定时器,如果新计时器覆盖了之前的计时器,alarm函数会返回上一个计时器剩余的时间
-SIGALARM信号效果 默认终止当前的进程
定时器:
定时器和进程的状态无关,即便进程处于阻塞态,alarm也会持续计时
*/
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
int setitimer (int which,const struct itimerval *new_val,struct itimerval *old_value);
/**定时器函数
参数:
-which 时间种类
ITIMER_REAL 自然时间(用户时间+内核时间+线程切换的时间等)->SIGALRM
ITIMER_VIRTUAL 用户时间 ->SIGVTALRM
ITIMER_PROF 用户时间+内核时间 ->SIGPROF信号
-*new_val 要设置的间隔信息
-*old_value 可以传入指针获取上次的定时器的参数,传入一个指针
关于itimerval
struct itimerval {
struct timeval it_interval; // 每个阶段的时间
struct timeval it_value; // 延迟多长时间执行定时器
};

struct timeval {
time_t tv_sec; // 秒
suseconds_t tv_usec; // 微妙
};
执行setitimer之后,经过it_value,开始执行定时器,每隔it_interval执行一次
*/
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <signal.h>

typedef void (*sighandler_t)(int);
/**回调函数
其实就相当于
void *sighandler(int num);
一个void *类型的函数,传入一个int型变量
*/
sighandler_t signal(int signum, sighandler_t handler);
/** 设置信号的默认处置方式
参数:
- signum 要捕捉的信号,建议使用宏,同一宏在不同平台可能对应不同值
- hanlder 捕捉到信号后处理方式
-SIG_IGN 忽略信号
-SIG_DFL 使用默认的行为
-回调函数:内核调用的信号处理函数,传入函数名即可
返回值:
成功则反映该信号上一次的回调函数地址,第一次调用返回NULL
失败则返回SIG_ERR
注意:
SIGKILL\SIGSTOP不可以被捕捉
*/

信号集
作用:存在于PCB中,表示一组不同的信号,其数据类型为sigset_t
相关函数如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
//以下的函数都是对自定义信号集进行操作
int sigemptyset(sigset_t *set);
/** 清空信号集中的数据,将所有标志位置0
参数:传入set指针,传出需要操作的信号集
返回值:
成功返回0,失败返回-1
*/
int sigfillset(sigset_t *set);
/** 所有标志位置1
参数:传入set指针,传出需要操作的信号集
返回值:
成功返回0,失败返回-1
*/
int sigaddset(sigset_t *set,int signum);
/** 设置信号机中某一个信号对应的标志位为1,表示阻塞这个信号
参数:
-set 传入set指针,传出需要操作的信号集
-signum 需要阻塞的信号
返回值:
成功返回0,失败返回-1
*/
int sigismember(const sigset_t *set,int signum);
/** 判断某个信号是否阻塞
参数:
-set 需要操作的信号集
-signum 需要判断的信号
返回值:
1:signum被阻塞
0:signum不阻塞
-1:调用失败
*/

信号捕捉与信号集处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
int sigprocmask(int how,const sigset_t *set, sigset_t *oldset);
/**
参数:
-how 如何对内核阻塞信号集进行处理
SIG_BLOCK 将用户设置的阻塞信号集添加到内核中,原有的不变,相当于取并集
SIG_UNBLOCK 根据用户进行的数据进行解除阻塞,相当于取反后并集
SIG_SETMASK 覆盖原有值
-*set 传入新的信号集
-*oldset 传出旧的信号集,可以是NULL
*/

int sigpending(sigset_t *set);
//获取未决信号集合的信息

int sigaction(int signum,const struct sigaction *act, struct sigaction *oldact);
//检查或改变信号的处理
/**
参数:
signum 需要捕捉的信号宏值
*act 新的处理动作
*oldact 接收原有的处理方法
struct sigaction{
void (*sa_handler)(int); 处理函数
void (*sa_sigaction)(int,siginfo_t *,void *); 不常用
sigset_t sa_mask; 在捕捉过程中使用的临时信号集
int sa_flags; 使用哪一个信号处理对捕捉到的信号进行处理,可以是0,表示使用sa_handler,也可以是SA_SIGINFO,表示使用sa_sigaction
void (*sa_restorer)(void); 废弃
}
*/

SIGCHLD信号
产生条件:

  • 子进程终止时
  • 子进程接收到SIGSTOP信号时
  • 子进程处在停止态,接收到SIGCONT唤醒时
    父进程默认忽略该信号,可用于解决僵尸进程的问题

共享内存

允许两个或多个进程共享物理内存的同一片区域,一个共享内存段会被称为进程用户空间的一部分,无需内核介入

使用方法

  • 调用shmget()创建一个新的共享内存段,返回一个共享内存标识符,也可以获取已经存在的标识符,会自动初始化内存中数据为0
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    int shmget(key_t key, size_t size, int shmflg);
    /**
    参数:
    -key 通过这个值找到或创建一个共享内存
    -size 共享内存的大小
    -shmflg 访问权限,并上八进制权限
    -IPC_CREAT 创建新内存段
    -IPC_EXCL 和creat一起使用,如果内存已经存在,该调用会失败
    返回值:
    成功返回>0,为共享内存引用ID
    失败返回-1
    */
  • 使用shmat()附上共享内存段,该端课称为调用进程的虚拟内存的一部分
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    void *shmat(int shmid,const void *shmaddr, int shmflg);
    /**
    参数
    -shmid 共享内存的标识,即shmget的返回值
    -shmaddr 申请的共享内存的起始地址,一般指定为null,由内核指定
    -shmflg 对共享内存的操作
    读:SHM_RDONLY
    读写:0
    返回值:
    成功返回共享内存的首地址
    失败返回(void*)-1
    */
  • 使用shmdt()分离共享内存块,调用后程序无法继续引用该内存块
    1
    2
    int shmdt(const void *shmaddr)
    参数:shmaddr,共享内存的首地址
  • 使用shmctl()删除共享内存块,只有所有进程都与之分离后才会被销毁,只有一个进程需要执行该步骤
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    int shmctl(int shmid, int cmd, struct shmid_ds *buf)
    /**
    参数:
    -shmid 需要被删除的共享内存的id
    -cmd 要做的操作
    IPC_STAT 获取共享内存当前的状态
    IPC_SET 设置共享内存的状态
    IPC_RMID 标记被销毁
    - *buf 需要设置或者获取的共享内存的属性信息
    -IPC_STAT buf存储数据
    -IPC_SET buf宏需要初始化数据
    -IPC_RMID 设置为NULL
    */
  • 使用ftok来获取key
    1
    2
    3
    4
    5
    6
    7
    key_t ftok(const char *pathname, int proj_id)
    /**
    参数
    -pathname:指定一个存在的路径
    -proj_id int类型的值,系统会使用其中的1个字节,范围0-255,一般指定一个字符'a'

    */

相关系统操作命令

  • ipcs
    • -a 查询所有的进程间通信行为
    • -m 查询所有共享内存行为
    • -q 查询所有消息队列行为
    • -s 查询所有管道
  • ipcrm
    • -M shmkey 删除shmkey创建的共享内存段
    • -m shmid 删除shmid标识的共享内存段
    • -Q msgkey 删除msqkey标识的消息队列
    • -q msqid 删除用msqid标识的消息队列
    • -S semkey 移除用semkey创建的信号
    • -s semid 移除用semid标识的信号

共享内存和内存映射的区别

  • 共享内存可以直接出房间,内存映射需要磁盘文件
  • 共享内存效率更高
  • 共享内存所有进程共享一块内存,内存映射每个进程在虚拟地址空间有一片独立的内存
  • 进程突然退出,共享内存还在,内存映射消失
  • 宕机,共享内存消失,内存映射会保存在文件里

生命周期

内存映射:进程退出,内存映射区销毁
共享内存:进程退出,共享内存还在,手动删除或关机

守护进程