程序就是一个文件,程序中的信息描述了如何创建一个进程。
图为进程虚拟地址空间分布。
并发:一个处理器在一个时刻只能处理一个命令,但多个进程指令被快速的轮换执行,使得在宏观上是同时执行的效果,其在微观上不是同时执行,只是把时间分成了若干段,使得每个命令可以交替的运行。
并行:同一时刻,多条指令在多个处理器上同时执行
内核为每个进程分配一个PCB(Processing Control Block)进程控制块,维护进程相关的信息,Linux 内核的进程控制块是task_struct 结构体。PCB在进程的内核区内。
进程状态反映进程执行过程的变化。这些状态随着进程的执行和外界条件的变化而转换。在三态模型中,进程状态分为三个基本状态,即就绪态,运行态,阻塞态。在五态模型中,进程分为新建态、就绪态,运行态,阻塞态,终止态。
**运行态:**进程占有处理器正在运行
**就绪态:**进程具备运行条件,等待系统分配处理器以便运行。当进程已分配到除CPU以外的所有必要资源后,只要再获得CPU,便可立即执行。在一个系统中处于就绪状态的进程可能有多个,通常将它们排成一个队列,称为就绪队列
**阻塞态:**又称为等待(wait)态或睡眠(sleep)态,指进程不具备运行条件,正在等待某个事件的完成
**新建态:**进程刚被创建时的状态,尚未进入就绪队列
**终止态:**进程完成任务到达正常结束点,或出现无法克服的错误而异常终止,或被操作系统及有终止权的进程所终止时所处的状态。进入终止态的进程以后不再执行,但依然保留在操作系统中等待善后。一旦其他进程完成了对终止态进程的信息抽取之后,操作系统将删除该进程。
ps aux / ajx
a:显示终端上的所有进程,包括其他用户的进程
u:显示进程的详细信息
x:显示没有控制终端的进程
j:列出与作业控制相关的信息
//状态的参数意义
D 不可中断 Uninterruptible(usually IO)
R 正在运行,或在队列中的进程
S 处于休眠状态
T 停止或被追踪
Z 僵尸进程
W 进入内存交换(从内核2.6开始无效)
X 死掉的进程
< 高优先级
N 低优先级
s 包含子进程
+ 位于前台的进程组
实时显示进程状态:
top
可以在使用 top 命令时加上 -d 来指定显示信息更新的时间间隔,在 top 命令执行后,可以按以下按键对显示的结果进行排序:
M 根据内存使用量排序
P 根据 CPU 占有率排序
T 根据进程运行时间长短排序
U 根据用户名来筛选进程
K 输入指定的 PID 杀死进程
每个进程都由进程号来标识,其类型为 pid_t(整型),进程号的范围:0~32767。进程号总是唯一的,但可以重用。当一个进程终止后,其进程号就可以再次使用。
任何进程(除 init 进程)都是由另一个进程创建,该进程称为被创建进程的父进程,对应的进程号称为父进程号(PPID)。
进程组是一个或多个进程的集合。他们之间相互关联,进程组可以接收同一终端的各种信号,关联的进程有一个进程组号(PGID)。默认情况下,当前的进程号会当做当前的进程组号。
pid_t getpid(void);//获取当前进程的pid
pid_t getppid(void); //获取父进程的pid
pid_t getpgid(pid_t pid);
进程创建
#include
#include
pid_t fork(void);
返回值:
成功:子进程中返回 0,父进程中返回子进程 ID
失败:返回 -1
失败的两个主要原因:
当前系统的进程数已经达到了系统规定的上限,这时 errno 的值被设置为 EAGAIN
系统内存不足,这时 errno 的值被设置为 ENOMEM
进程的虚拟内存空间
操作系统会给每一个进程都分配一个虚拟内存空间,其在逻辑上是连续的但是在物理上不一定是连续的。
fock函数:
/*
#include
#include
pid_t fork(void);
作用:用于创建子进程
返回值:
fork()的返回值会返回两次,一次在父进程中,一次在子进程中,父进程中返回值为子进程的PID。
在子进程中返回0
通过fork()的返回值区分父进程和子进程
在父进程中返回-1,表示创建子进程失败,并且设置errno
*/
#include
#include
#include
int main() {
int num = 10;
// 创建子进程
pid_t pid = fork();
// 判断是父进程还是子进程
if(pid > 0) {
// printf("pid : %d\n", pid);
// 如果大于0,返回的是创建的子进程的进程号,当前是父进程
printf("i am parent process, pid : %d, ppid : %d\n", getpid(), getppid());
printf("parent num : %d\n", num);
num += 10;
printf("parent num += 10 : %d\n", num);
} else if(pid == 0) {
// 当前是子进程
printf("i am child process, pid : %d, ppid : %d\n", getpid(),getppid());
printf("child num : %d\n", num);
num += 100;
printf("child num += 100 : %d\n", num);
}
// for循环
for(int i = 0; i < 3; i++) {
printf("i : %d , pid : %d\n", i , getpid());
sleep(1);
}
return 0;
}
上面程序的结果为:
Num在父进程中由1 + 10 变为 11, 在子进程中写时拷贝1 + 100 = 101。并且父进程和子进程都进入下面的循环中,但是两个进程相互不影响。
实际上,准确来说,Linux的fock()使用是通过**写时拷贝(copy-on-write)**实际内核并不复制整个个进程的地址空间,而是让父子进程共享一个地址空间只用在需要写入时才会复制地址空间,从而使各个进程间拥有各自的地址空间也就是说,资源的复制是需要写入时才会进行,在此之前只用制度方式共享
注意;fock之后父子进程共享文件fock产生的子进程和父进程相同的文件描述符指向相同的文件表,引用计数增加,共享文件偏移指针。
exec族函数:
exec 函数族的作用是根据指定的文件名找到可执行文件,并用它来取代调用进程的内容,换句话说,就是在调用进程内部执行一个可执行文件。
exec 函数族的函数执行成功后不会返回,因为调用进程的实体,包括代码段,数据段和堆栈等都已经被新的内容取代,只留下进程 ID 等一些表面上的信息仍保持原样,颇有些神似“三十六计”中的“金蝉脱壳”。看上去还是旧的躯壳,却已经注入了新的灵魂。只有调用失败了,它们才会返回 -1,从原程序的调用点接着往下执行。
#include
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[]);
#include
int execl(const char *path, const char *arg, ...);
- 参数:
- path:需要指定的执行的文件的路径或者名称
- arg:是执行可执行文件所需要的参数列表
第一个参数一般没有什么作用,为了方便,一般写的是执行的程序的名称
从第二个参数开始往后,就是程序执行所需要的的参数列表。
参数最后需要以NULL结束(哨兵)
- 返回值:
只有当调用失败,才会有返回值,返回-1,并且设置errno
如果调用成功,没有返回值。
#include
int execlp(const char *file, const char *arg, ... );
- 会到环境变量中查找指定的可执行文件,如果找到了就执行,找不到就执行不成功。
- 参数:
- file:需要执行的可执行文件的文件名
- arg:是执行可执行文件所需要的参数列表
第一个参数一般没有什么作用,为了方便,一般写的是执行的程序的名称
从第二个参数开始往后,就是程序执行所需要的的参数列表。
参数最后需要以NULL结束(哨兵)
- 返回值:
只有当调用失败,才会有返回值,返回-1,并且设置errno
如果调用成功,没有返回值。
进程退出:
进程退出在C库中调用exit()函数,Linux调用_exit()函数,两者的区别在于,exit()函数的底层也是调用__exit(),只不过C库加入了缓冲区刷新。
#include
void exit(int status);
#include
void _exit(int status);
status参数:是进程退出时的一个状态信息。父进程回收子进程资源的时候可以获取到。
#include
#include
#include
int main () {
printf("hello\n");
printf("world");//不刷新缓冲区
_exit(0);
//exit(0);
return 0;
}
Linux中的exit_(),不刷新缓冲区。
C库中的exit(),刷新缓冲区。
父进程运行结束,但子进程还在运行(未运行结束),这样的子进程就称为孤儿进程(Orphan Process)。
内核将Init设为孤儿进程的父进程,Init就会领养孤儿进程,Init进程循环的调用wait()释放孤儿进程的资源。
#include
#include
#include
int main() {
// 创建子进程
pid_t pid = fork();
// 判断是父进程还是子进程
if(pid > 0) {
printf("i am parent process, pid : %d, ppid : %d\n", getpid(), getppid());
} else if(pid == 0) {
sleep(20);//sleep使父进程先结束
// 当前是子进程
printf("i am child process, pid : %d, ppid : %d\n", getpid(),getppid());
}
return 0;
}
这时由于父进程已经结束,子进程变为孤儿进程,init变为其父进程,释放子进程的资源。
子进程的内核区的PCB块只能由其父进程进行释放,而若进程终止时,父进程尚未回收,子进程残留资源(PCB)存放于内核中,变成僵尸进程。
僵尸进程不能被kill-9
杀掉,父进程不调用wait()或waitpid(),保留的信息就不会被释放,其进程号就会被占用,系统所能使用的进程号是有限的,如果大量生产僵尸进程,将会因为没有可用的进程好导致系统不能产生新的进程。
父进程进入死循环,子进程终止但无法被回收资源就会变成僵尸进程,图上子进程显示状态为Z就是僵尸进程状态。
进程回收
在每个进程退出的时候,内核释放该进程所有的资源、包括打开的文件、占用的内存等。但是仍然为其保留一定的信息,这些信息主要主要指进程控制块PCB的信息(包括进程号、退出状态、运行时间等)。
父进程可以通过调用 wait 或 waitpid 得到它的退出状态同时彻底清除掉这个进程。
wait() 和 waitpid() 函数的功能一样,区别在于,wait() 函数会阻塞,waitpid() 可以设置不阻塞。
waitpid() 还可以指定等待哪个子进程结束。
如果没有子进程结束wait()就会阻塞代码然后回收,而waitpit()会一直等待某个子进程结束再进行回收。
#include
#include
pid_t wait(int *wstatus);
功能:等待任意一个子进程结束,如果任意一个子进程结束了,次函数会回收子进程的资源。
参数:int *wstatus
进程退出时的状态信息,传入的是一个int类型的地址,传出参数。
返回值:
- 成功:返回被回收的子进程的id
- 失败:-1 (所有的子进程都结束,调用函数失败)
调用wait函数的进程会被挂起(阻塞),直到它的一个子进程退出或者收到一个不能被忽略的信号时才被唤醒(相当于继续往下执行)
如果没有子进程了,函数立刻返回,返回-1;如果子进程都已经结束了,也会立即返回,返回-1.
#include
#include
pid_t waitpid(pid_t pid, int *wstatus, int options);
功能:回收指定进程号的子进程,可以设置是否阻塞。
参数:
- pid:
pid > 0 : 某个子进程的pid
pid = 0 : 回收当前进程组的所有子进程
pid = -1 : 回收所有的子进程,相当于 wait() (最常用)
pid < -1 : 某个进程组的组id的绝对值,回收指定进程组中的子进程
- options:设置阻塞或者非阻塞
0 : 阻塞
WNOHANG : 非阻塞
- 返回值:
> 0 : 返回子进程的id
= 0 : options=WNOHANG, 表示还有子进程或者
= -1 :错误,或者没有子进程了
进程是一个独立的资源分配单位,不同的进程之间的资源是相互独立、没有关联的,不能在一个进程中直接访问另一个进程的资源。
但进程不是孤立的,不同的进程之间需要进行信息的交互和状态的转递,因此需要进程间通信( IPC:
Inter Processes Communication)。
进程间通信的目的:
管道是UNIX系统进程间通信最古老的形式,所有的UNIX都支持这种通信方式。
ls | wc-l
这里的|就是管道符,表示ls进程通过管道传递信息到wc进程中。
管道的特点
在父进程读时其写信号要关闭,写时读信号要关闭,子进程一样。
创建匿名管道
#include
int pipe(int pipefd[2]);
查看管道缓冲大小命令
ulimit -a
#include
int pipe(int pipefd[2]);
功能:创建一个匿名管道,用来进程间通信。
参数:int pipefd[2] 这个数组是一个传出参数。
pipefd[0] 对应的是管道的读端
pipefd[1] 对应的是管道的写端
返回值:
成功 0
失败 -1
管道默认是阻塞的:如果管道中没有数据,read阻塞,如果管道满了,write阻塞
FIFO 严格遵循先进先出(First in First out),对管道及 FIFO 的读总是从开始处返回数据,对它们的写则把数据添加到末尾。它们不支持诸如 lseek() 等文件定位操作。
创建管道
#include
#include
int mkfifo(const char *pathname, mode_t mode);
内存映射(Memory-mapped I/O)是将磁盘文件的数据映射到内存,用户通过修改内存就能修改磁盘文件。
某进程可以通过修改文件进行进程间通信。
#include
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
int munmap(void *addr, size_t length);
#include
void *mmap(void *addr, size_t length, int prot, int flags,int fd, off_t offset);
- 功能:将一个文件或者设备的数据映射到内存中
- 参数:
- void *addr: NULL, 由内核指定
- length: 要映射的数据的长度,这个值不能为0。建议使用文件的长度。
获取文件的长度:stat lseek
- prot: 对申请的内存映射区的操作权限
-PROT_EXEC :可执行的权限
-PROT_READ :读权限
-PROT_WRITE :写权限
-PROT_NONE :没有权限
要操作映射内存,必须要有读的权限。
PROT_READ、PROT_READ|PROT_WRITE
- flags:
- MAP_SHARED : 映射区的数据会自动和磁盘文件进行同步,进程间通信,必须要设置这个选项
- MAP_PRIVATE :不同步,内存映射区的数据改变了,对原来的文件不会修改,会重新创建一个新的文件。(copy on write)
- fd: 需要映射的那个文件的文件描述符
- 通过open得到,open的是一个磁盘文件
- 注意:文件的大小不能为0,open指定的权限不能和prot参数有冲突。
prot: PROT_READ open:只读/读写
prot: PROT_READ | PROT_WRITE open:读写
- offset:偏移量,一般不用。必须指定的是4k的整数倍,0表示不偏移。
- 返回值:返回创建的内存的首地址
失败返回MAP_FAILED,(void *) -1
int munmap(void *addr, size_t length);
- 功能:释放内存映射
- 参数:
- addr : 要释放的内存的首地址
- length : 要释放的内存的大小,要和mmap函数中的length参数的值一样。
使用内存映射实现进程间通信:
1.有关系的进程(父子进程)
还没有子进程的时候,通过唯一的父进程,先创建内存映射区,有了内存映射区以后,创建子进程,父子进程共享创建的内存映射区。
2.没有关系的进程间通信
准备一个大小不是0的磁盘文件,通过磁盘文件创建内存映射区,得到一个操作这块内存的指针,进程2 通过磁盘文件创建内存映射区,得到一个操作这块内存的指针,使用内存映射区通信。
注意:内存映射区通信,是非阻塞。
#include
#include
#include
#include
#include
#include
#include
#include
int main () {
// 打开文件
int fd = open("test.txt", O_RDWR);
int size = lseek(fd, 0, SEEK_END);
// 创建内存映射区
void* ptr = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (ptr == MAP_FAILED) {
perror("mmap");
exit(0);
}
// 创建子进程
pid_t pid = fork();
if (pid > 0) {
wait(NULL);
char buf[64];
strcpy(buf, (char*)ptr);
printf("read data: %s\n", buf);// 从内存映射区读
}else if(pid == 0) {
strcpy((char*)ptr,"hello i am son");// 写入内存映射区
}
// 关闭映射区
munmap(ptr, size);
return 0;
}
共享内存允许两个或者多个进程共享物理内存的同一块区域(通常被称为段)。由于一个共享内存段会称为一个进程用户空间的一部分,因此这种 IPC 机制无需内核介入。所有需要做的就是让一个进程将数据复制进共享内存中,并且这部分数据会对其他所有共享同一个段的进程可用。与管道等要求发送进程将数据从用户空间的缓冲区复制进内核内存和接收进程将数据从内核内存复制进用户空间的缓冲区的做法相比,这种 IPC 技术的速度更快。
int shmget(key_t key, size_t size, int shmflg); //key为键值是通信的标识,创建一块共享内存,shmflg为状态码
void *shmat(int shmid, const void *shmaddr, int shmflg); //进程与内存关联,shmaddr为进程,返回值为这块共享内存的地址
int shmdt(const void *shmaddr); //解除关联
int shmctl(int shmid, int cmd, struct shmid_ds *buf); //删除共享内存
key_t ftok(const char *pathname, int proj_id);
共享内存的效率要大于内存映射效率。
ipcs 用法
ipcs -a // 打印当前系统中所有的进程间通信方式的信息
ipcs -m // 打印出使用共享内存进行进程间通信的信息
ipcs -q // 打印出使用消息队列进行进程间通信的信息
ipcs -s // 打印出使用信号进行进程间通信的信息
ipcrm 用法
ipcrm -M shmkey // 移除用shmkey创建的共享内存段
ipcrm -m shmid // 移除用shmid标识的共享内存段
ipcrm -Q msgkey // 移除用msqkey创建的消息队列
ipcrm -q msqid // 移除用msqid标识的消息队列
ipcrm -S semkey // 移除用semkey创建的信号
ipcrm -s semid // 移除用semid标识的信号
#include
#include
#include
#include
int main() {
// 1.创建一个共享内存
int shmid = shmget(100, 4096, IPC_CREAT|0664);
printf("shmid : %d\n", shmid);
// 2.和当前进程进行关联
void * ptr = shmat(shmid, NULL, 0);
char * str = "helloworld";
// 3.写数据
memcpy(ptr, str, strlen(str) + 1);
printf("按任意键继续\n");
getchar();
// 4.解除关联
shmdt(ptr);
// 5.删除共享内存
shmctl(shmid, IPC_RMID, NULL);
return 0;
}
#include
#include
#include
#include
int main() {
// 1.获取一个共享内存
int shmid = shmget(100, 0, IPC_CREAT);
printf("shmid : %d\n", shmid);
// 2.和当前进程进行关联
void * ptr = shmat(shmid, NULL, 0);
// 3.读数据
printf("%s\n", (char *)ptr);
printf("按任意键继续\n");
getchar();
// 4.解除关联
shmdt(ptr);
// 5.删除共享内存
shmctl(shmid, IPC_RMID, NULL);
return 0;
}
ipcs-a显示所有进程间通信的方式,分别是消息队列,内存共享,信号,可以看到我们现在两个进程在使用这块创建的内存块,这块内存块通过key值进行配对。
信号是 Linux 进程间通信的最古老的方式之一,是事件发生时对进程的通知机制,有时也称之为软件中断,它是在软件层次上对中断机制的一种模拟,是一种异步通信的方式。信号可以导致一个正在运行的进程被另一个正在运行的异步进程中断,转而处理某一个突发事件。
引发内核为进程产生信号的各类事件如下:
使用信号的两个主要目的是:
信号的特点:
查看系统定义的信号列表:kill –l
前 31 个信号为常规信号,其余为实时信号。
编号 | 信号名称 | 对应事件 | 默认动作 |
---|---|---|---|
1 | SIGHUP | 用户退出shell时,由该shell启动的所有进程将收到这个信号 | 终止进程 |
2 | SIGINT | 当用户按下了 |
终止进程 |
3 | SIGQUIT | 用户按下 |
终止进程 |
4 | SIGILL | CPU检测到某进程执行了非法指令 | 终止进程并产生core文件 |
5 | SIGTRAP | 该信号由断点指令或其他 trap指令产生 | 终止进程并产生core文件 |
6 | SIGABRT | 调用abort函数时产生该信号 | 终止进程并产生core文件 |
7 | SIGBUS | 非法访问内存地址,包括内存对齐出错 | 终止进程并产生core文件 |
8 | SIGFPE | 在发生致命的运算错误时发出。不仅包括浮点运算错误,还包括溢出及除数为0等所有的算法错误 | 终止进程并产生core文件 |
9 | SIGKILL | 无条件终止进程。该信号不能被忽略,处理和阻塞 | 终止进程,可以杀死任何进程 |
10 | SIGUSE1 | 用户定义的信号。即程序员可以在程序中定义并使用该信号 | 终止进程 |
11 | SIGSEGV | 指示进程进行了无效内存访问(段错误) | 终止进程并产生core文件 |
12 | SIGUSR2 | 另外一个用户自定义信号,程序员可以在程序中定义并使用该信号 | 终止进程 |
13 | SIGPIPE | Broken pipe向一个没有读端的管道写数据 | 终止进程 |
14 | SIGALRM | 定时器超时,超时的时间 由系统调用alarm设置 | 终止进程 |
15 | SIGTERM | 程序结束信号,与SIGKILL不同的是,该信号可以被阻塞和终止。通常用来要示程序正常退出。执行shell命令Kill时,缺省产生这个信号 | 终止进程 |
16 | SIGSTKFLT | Linux早期版本出现的信号,现仍保留向后兼容 | 终止进程 |
17 | SIGCHLD | 子进程结束时,父进程会收到这个信号 | 忽略这个信号 |
18 | SIGCONT | 如果进程已停止,则使其继续运行 | 继续/忽略 |
19 | SIGSTOP | 停止进程的执行。信号不能被忽略,处理和阻塞 | 为终止进程 |
20 | SIGTSTP | 停止终端交互进程的运行。按下 |
暂停进程 |
21 | SIGTTIN | 后台进程读终端控制台 | 暂停进程 |
22 | SIGTTOU | 该信号类似于SIGTTIN,在后台进程要向终端输出数据时发生 | 暂停进程 |
23 | SIGURG | 套接字上有紧急数据时,向当前正在运行的进程发出些信号,报告有紧急数据到达。如网络带外数据到达 | 忽略该信号 |
24 | SIGXCPU | 进程执行时间超过了分配给该进程的CPU时间,系统产生该信号并发送给该进程 | 终止进程 |
25 | SIGXFSZ | 超过文件的最大长度设置 | 终止进程 |
26 | SIGVTALRM | 虚拟时钟超时时产生该信号。类似于SIGALRM,但是该信号只计算该进程占用CPU的使用时间 | 终止进程 |
27 | SGIPROF | 类似于SIGVTALRM,它不公包括该进程占用CPU时间还包括执行系统调用时间 | 终止进程 |
28 | SIGWINCH | 窗口变化大小时发出 | 忽略该信号 |
29 | SIGIO | 此信号向进程指示发出了一个异步IO事件 | 忽略该信号 |
30 | SIGPWR | 关机 | 终止进程 |
31 | SIGSYS | 无效的系统调用 | 终止进程并产生core文件 |
34 ~ 64 | SIGRTMIN~SIGRTMAX | LINUX的实时信号,它们没有固定的含义(可以由用户自定义) | 终止进程 |
查看信号的详细信息
man 7 signal
信号的 5 中默认处理动作
Term 终止进程
Ign 当前进程忽略掉这个信号
Core 终止进程,并生成一个Core文件
Stop 暂停当前进程
Cont 继续执行当前被暂停的进程
Core作用:
通过gdb调试可以直接找到程序错误所在。
int kill(pid_t pid, int sig); //向进程发送信号
int raise(int sig); //向当前进程发送信息
void abort(void); //发送SIGABRT信号给当前的进程,杀死当前进程
unsigned int alarm(unsigned int seconds); //定时器
int setitimer(int which, const struct itimerval *new_val, struct itimerval *old_value);//定时器
#include
#include
int kill(pid_t pid, int sig);
- 功能:给任何的进程或者进程组pid, 发送任何的信号sig
- 参数:
- pid:
> 0 : 将信号发送给指定的进程
= 0 : 将信号发送给当前的进程组
= -1 : 将信号发送给每一个有权限接收这个信号的进程
< -1 : 这个pid=某个进程组的ID取反 (-12345)
- sig : 需要发送的信号的编号或者是宏值,0表示不发送任何信号
kill(getppid(), 9);
kill(getpid(), 9);
int raise(int sig);
- 功能:给当前进程发送信号
- 参数:
- sig : 要发送的信号
- 返回值:
- 成功 0
- 失败 非0
kill(getpid(), sig);
void abort(void);
- 功能: 发送SIGABRT信号给当前的进程,杀死当前进程
kill(getpid(), SIGABRT);
#include
unsigned int alarm(unsigned int seconds);
- 功能:设置定时器(闹钟)。函数调用,开始倒计时,当倒计时为0的时候,
函数会给当前的进程发送一个信号:SIGALARM
- 参数:
seconds: 倒计时的时长,单位:秒。如果参数为0,定时器无效(不进行倒计时,不发信号)。
取消一个定时器,通过alarm(0)。
- 返回值:
- 之前没有定时器,返回0
- 之前有定时器,返回之前的定时器剩余的时间
- SIGALARM :默认终止当前的进程,每一个进程都有且只有唯一的一个定时器。
alarm(10); -> 返回0
过了1秒
alarm(5); -> 返回9
alarm()函数是不阻塞的
#include
int setitimer(int which, const struct itimerval *new_value,
struct itimerval *old_value);
- 功能:设置定时器(闹钟)。可以替代alarm函数。精度微妙us,可以实现周期性定时
- 参数:
- which : 定时器以什么时间计时
ITIMER_REAL: 真实时间,时间到达,发送 SIGALRM 常用
ITIMER_VIRTUAL: 用户时间,时间到达,发送 SIGVTALRM
ITIMER_PROF: 以该进程在用户态和内核态下所消耗的时间来计算,时间到达,发送 SIGPROF
- new_value: 设置定时器的属性
struct itimerval { // 定时器的结构体
struct timeval it_interval; // 每个阶段的时间,间隔时间
struct timeval it_value; // 延迟多长时间执行定时器
};
struct timeval { // 时间的结构体
time_t tv_sec; // 秒数
suseconds_t tv_usec; // 微秒
};
过10秒后,每个2秒定时一次
- old_value :记录上一次的定时的时间参数,一般不使用,指定NULL
- 返回值:
成功 0
失败 -1 并设置错误号
alarm使用
#include
#include
int main () {
int seconds = alarm(5);
printf("seconds = %d\n", seconds);// 之前没有定时器所以返回0
sleep(2);
seconds = alarm(2);
printf("second = %d\n", seconds);// 之前有定时器返回定时器剩余的时间3
while(1) {
}
}
通过alarm测试计算机1秒可以打印多少数字
#include
#include
int main() {
alarm(1);
int i = 0;
while(1) {
printf("%i\n", i++);
}
return 0;
}
实际的时间 = 内核时间 + 用户时间 + 消耗的时间 (用户态和内核态进行切换的时间,系统调用的时间,写入数据的时间)
进行文件IO操作的时候比较浪费时间
定时器,与进程的状态无关(自然定时法)。无论进程处于什么状态,alarm都会计时。
sighandler_t signal(int signum, sighandler_t handler);
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
#include
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
- 功能:设置某个信号的捕捉行为
- 参数:
- signum: 要捕捉的信号
- handler: 捕捉到信号要如何处理
- SIG_IGN : 忽略信号
- SIG_DFL : 使用信号默认的行为
- 回调函数 : 这个函数是内核调用,程序员只负责写,捕捉到信号后如何去处理信号。
回调函数:
- 需要程序员实现,提前准备好的,函数的类型根据实际需求,看函数指针的定义
- 不是程序员调用,而是当信号产生,由内核调用
- 函数指针是实现回调的手段,函数实现之后,将函数名放到函数指针的位置就可以了。
- 返回值:
成功,返回上一次注册的信号处理函数的地址。第一次调用返回NULL
失败,返回SIG_ERR,设置错误号
SIGKILL SIGSTOP不能被捕
=
signal函数示例
#include
#include
#include
#include
void myalarm(int num) {
printf("捕捉的的信号的编号是:%d\n",num);
printf("xxxxxxx\n");
}
// 过3秒后,每隔2秒钟定时一次
int main () {
signal(SIGALRM, myalarm);//捕捉信号
struct itimerval new_value;
//设置间隔的时间
new_value.it_interval.tv_sec = 2;
new_value.it_interval.tv_usec = 0;
//设置延迟的时间
new_value.it_value.tv_sec = 3;
new_value.it_value.tv_usec = 0;
int ret = setitimer(ITIMER_REAL, &new_value, NULL);
printf("定时器开始。。。\n");
if (ret == -1) {
perror("setitimer");
exit(0);
}
getchar();
return 0;
}
sigaction函数示例
#include
int sigaction(int signum, const struct sigaction *act,
struct sigaction *oldact);
- 功能:检查或者改变信号的处理。信号捕捉
- 参数:
- signum : 需要捕捉的信号的编号或者宏值(信号的名称)
- act :捕捉到信号之后的处理动作
- oldact : 上一次对信号捕捉相关的设置,一般不使用,传递NULL
- 返回值:
成功 0
失败 -1
struct sigaction {
// 函数指针,指向的函数就是信号捕捉到之后的处理函数
void (*sa_handler)(int);
// 不常用
void (*sa_sigaction)(int, siginfo_t *, void *);
// 临时阻塞信号集,在信号捕捉函数执行过程中,临时阻塞某些信号。
sigset_t sa_mask;
// 使用哪一个信号处理对捕捉到的信号进行处理
// 这个值可以是0,表示使用sa_handler,也可以是SA_SIGINFO表示使用sa_sigaction
int sa_flags;
// 被废弃掉了
void (*sa_restorer)(void);
};
#include
#include
#include
#include
void myalarm (int num) {
printf("捕捉到的信号编号是: %d\n", num);
printf("xxxxxxxxx\n");
}
int main () {
struct sigaction act;
act.sa_flags = 0;
act.sa_handler = myalarm;
sigemptyset(&act.sa_mask); //清空临时阻塞信号集
struct itimerval new_val;
//设置间隔的时间
new_val.it_interval.tv_sec = 2;
new_val.it_interval.tv_usec = 0;
//设置延迟时间
new_val.it_value.tv_sec = 3;
new_val.it_value.tv_usec = 0;
int ret = setitimer(ITIMER_REAL, &new_val, NULL);
printf("定时器开始.....\n");
if (ret == -1) {
perror("setitimer");
exit(0);
}
while(1);
return 0;
}
许多信号相关的系统调用都需要能表示一组不同的信号,多个信号可使用一个称之为信号集的数据结构来表示,其系统数据类型为 sigset_t。
在 PCB 中有两个非常重要的信号集。一个称之为 “阻塞信号集” ,另一个称之为 “未决信号集” 。这两个信号集都是内核使用位图机制来实现的。但操作系统不允许我们直接对这两个信号集进行位操作。而需自定义另外一个集合,
借助信号集操作函数来对 PCB 中的这两个信号集进行修改。
信号的 “未决” 是一种状态,指的是从信号的产生到信号被处理前的这一段时间。
信号的 “阻塞” 是一个开关动作,指的是阻止信号被处理,但不是阻止信号产生。
信号的阻塞就是让系统暂时保留信号留待以后发送。由于另外有办法让系统忽略信号,所以一般情况下信号的阻塞只是暂时的,只是为了防止信号打断敏感的操作。
1.用户通过键盘 Ctrl + C, 产生2号信号SIGINT (信号被创建)
2.信号产生但是没有被处理 (未决)
- 在内核中将所有的没有被处理的信号存储在一个集合中 (未决信号集)
- SIGINT信号状态被存储在第二个标志位上
- 这个标志位的值为0, 说明信号不是未决状态
- 这个标志位的值为1, 说明信号处于未决状态
3.这个未决状态的信号,需要被处理,处理之前需要和另一个信号集(阻塞信号集),进行比较
4.在处理的时候和阻塞信号集中的标志位进行查询,看是不是对该信号设置阻塞了
int sigemptyset(sigset_t *set);//全部设为空
int sigfillset(sigset_t *set); //全部设为1
int sigaddset(sigset_t *set, int signum);//设置某个信号
int sigdelset(sigset_t *set, int signum); //删除某个信号
int sigismember(const sigset_t *set, int signum);//判断某个信号是否阻塞
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);//操作内核中的阻塞信号集接口
int sigpending(sigset_t *set);//接收内核未决信号集
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:传出参数,需要操作的信号集
- signum:需要设置阻塞的那个信号
- 返回值:成功返回0, 失败返回-1
int sigdelset(sigset_t *set, int signum);
- 功能:设置信号集中的某一个信号对应的标志位为0,表示不阻塞这个信号
- 参数:
- set:传出参数,需要操作的信号集
- signum:需要设置不阻塞的那个信号
- 返回值:成功返回0, 失败返回-1
int sigismember(const sigset_t *set, int signum);
- 功能:判断某个信号是否阻塞
- 参数:
- set:需要操作的信号集
- signum:需要判断的那个信号
- 返回值:
1 : signum被阻塞
0 : signum不阻塞
-1 : 失败
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
- 功能:将自定义信号集中的数据设置到内核中(设置阻塞,解除阻塞,替换)
- 参数:
- how : 如何对内核阻塞信号集进行处理
SIG_BLOCK: 将用户设置的阻塞信号集添加到内核中,内核中原来的数据不变
假设内核中默认的阻塞信号集是mask, mask | set
SIG_UNBLOCK: 根据用户设置的数据,对内核中的数据进行解除阻塞
mask &= ~set
SIG_SETMASK:覆盖内核中原来的值
- set :已经初始化好的用户自定义的信号集
- oldset : 保存设置之前的内核中的阻塞信号集的状态,可以是 NULL
- 返回值:
成功:0
失败:-1
设置错误号:EFAUL
int sigpending(sigset_t *set);
- 功能:获取内核中的未决信号集
- 参数:set,传出参数,保存的是内核中的未决信号集中的信息。
打印未决信号集中的内容
#include
#include
#include
#include
int main () {
// 设置2、3号信号阻塞
sigset_t set;
sigemptyset(&set);// 将随机的信号集设为0
// 将2号和3号信号添加到set信号集中
sigaddset(&set, SIGINT);
sigaddset(&set, SIGQUIT);
// 修改内核中的阻塞信号集(用set信号集设置)
sigprocmask(SIG_BLOCK, &set, NULL);
int num = 0;
while(1) {
num++;
// 获取当前的未决信号集的数据
sigset_t pendingset;
sigemptyset(&);// 将信号集设为0
sigpending(&pendingset);// 将内核中的信号集输出到自设的信号集中
// 遍历信号集的前32位
for (int i = 1; i <= 31; i++) {
if (sigismember(&pendingset, i) == 1) {
printf("1");
}else if (sigismember(&pendingset, i) == 0) {
printf("0");
}else {
perror("sigismember");
exit(0);
}
}
printf("\n");
sleep(1);
// 接触信号阻塞状态退出遍历
if (num == 10) {
sigprocmask(SIG_UNBLOCK, &set, NULL);
}
}
return 0;
}