程序,是指编译好的二进制文件,在磁盘上,不占用系统资源(CPU、内存、打开的文件、设备、锁等等)。
进程,是一个抽象的概念,与操作系统原理联系紧密。进程是活跃的程序,占用系统资源。在内存中执行(程序运行起来,产生一个进程)。
程序 --> 剧本(纸),进程 -->戏(舞台、演员、灯光、道具等等)。同一个剧本可以在多个舞台同时上演。同样,同个程序也可以加载为不同的进程(彼此之间互不影响)。如:同时开两个终端。各自都有一个bash,但彼此ID不同。
并发,在操作系统中,一个时间段中有多个进程都处于已启动运行到运行完毕之间的状态。但任一个时刻点上仍只有一个进程在运行。
例如,当下,我们使用计算机时可以边听音乐边聊天上网。若笼统的将他们均看做一个进程的话,为什么可以同时运行呢?因为并发。
分时复用CPU
在计算机内存中同时存放几道相互独立的程序,它们在管理程序控制之下,相互穿插的运行。多道程序设计必须有硬件基础作为保证。
时钟中断即为多道程序设计模型的理论基础。并发时,任意进程在执行期间都不希望放弃CPU。因此系统需要一种强制让进程让出CPU资源的手段。时钟中断有硬件基础作为保障,对进程而言不可抗拒。操作系统中的中断处理函数,来负责调度程序执行。
在多道程序设计模型中,多个进程轮流使用CPU(分时复用CPU资源)。而当下常见CPU为纳米级,1秒可以执行大约10亿条指令。由于人眼的反应速度是毫秒级,所以看似同时在运行。
1s = 1000ms
1ms = 1000us
1us = 1000ms
实质上,并发是宏观并行,微观串行! -- 推动了计算机蓬勃发展,将人类引入了多媒体时代。
我们知道,每个进程在内核中都有一个进程控制块(PCB)来维护进程相关的信息,Linux内核的进程控制块是task_struct结构体。
练习:打印当前进程的所有环境变量。
#include
extern char **environ;
int main(int argc, char *argv[])
{
int i;
for(i = 0; environ[i]; i++)
{
printf("%s\n", environ[i]);
}
return 0;
}
可执行文件的搜索路径。ls命令也是一个程序,执行它不需要提供完整的路径名/bin/ls,然而通常我们执行当前目录下的程序a.out却需要提供完整的路径名./a.out,这是因为PATH环境变量的值里面包含了ls命令所在的目录/bin,却不包含a.out所在的目录。PATH环境变量的值可以包含多个目录,用:号隔开。在shell中用echo命令可以查看这个环境变量的值:
echo $PATH
获取环境变量
char *getenv(const char *name);
练习:编程实现getenv函数。
设置环境变量的值
int setenv(const char *name, const char * value, int overwrite);
删除环境变量name的定义
int unsetenv(const char *name);
示例
#include
#include
#include
int main(int argc, char * argv[])
{
char * val;
const char * name = "ABD";
val = getenv(name);
printf("1, %s = %s\n", name, val);//ABD = NULL
setenv(name, "efg", 1);
val = getenv(name);
printf("2, %s = %s\n", name, val);//ABD = efg
int ret = unsetenv(name);
printf("ret = %d \n", ret);//0
val = getenv(name);
printf("3, %s = %s \n", name, val);//ABD = NULL
return 0;
}
示例
#include
#include
#include
int main(int argc, char *argv[])
{
printf("father process exec begin...");
pid_t pid = fork();
if(pid == -1)
{
perror("fork error");
exit(1);
}
else if(pid == 0)
{
printf("I'm child, pid = %u, ppid = %u \n", getpid(), getppid());
}
else
{
printf("I'm father, pid = %u, ppid = %u \n", getpid(), getppid());
sleep(1);
}
printf("father process exec end...");
return 0;
}
错误示例
#include
#include
#include
int main(int argc, char *argv[])
{
printf("father process exec begin...");
pid_t pid;
int i;
for(i = 0; i < 5; i++)
{
pid_t pid = fork();
if(pid == -1)
{
perror("fork error");
exit(1);
}
else if(pid == 0)
{
printf("I'm %dth child, pid = %u, ppid = %u \n", i+1, getpid(), getppid());
}
else
{
printf("I'm father, pid = %u, ppid = %u \n", getpid(), getppid());
sleep(1);
}
}
printf("father process exec end...");
return 0;
}
正确的调用方式
#include
#include
#include
int main(int argc, char *argv[])
{
printf("father process exec begin...");
pid_t pid;
int i;
for(i = 0; i < 5; i++)
{
pid_t pid = fork();
if(pid == -1)
{
perror("fork error");
exit(1);
}
else if(pid == 0)
{
//不让子进程现创建孙子进程
break;
}
}
if(i<5)
{
sleep(i);
printf("I'm %dth child, pid = %u, ppid = %u \n", i+1, getpid(), getppid());
}
else
{
sleep(i);
printf("I'm father");
}
return 0;
}
pid_t getpid(void);
pid_t getppid(void);
uid_t getuid(void);
uid_t geteuid(void);
gid_t getgid(void);
gid_t getegid(void);
set follow-fork-mode child
命令设置gdb在fork之后跟踪子进程。set follow-fork-mode parent
设置跟踪父进程。fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支), 子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。
将当前进程的.text、.data替换为所要加载的程序的.text、.data,然后让进程从新的.text第一条指令开始执行,但进程ID不变,换核不换壳。
加载一个进程,借助PATH环境变量
int execlp(const char file, const char arg, ...); 成功:无返回;失败:-1。
参数1:要加载的程序的名字。该函数需要配合PATH环境变量来使用,当PAHT中所有目录搜索后没有参数1则出错返回。
该函数通常用来调用系统程序。如:ls、date、cp、cat等命令。
示例
#include
#include
#include
int main(int argc, char * argv[])
{
pid_t pid;
pid = fork();
if(pid == -1)
{
perror("fork error");
exit(1);
}
else if (pid > 0)
{
sleep(1);
printf("parent");
}
else
{
execlp("ls", "ls", "-l", "-a", NULL);
}
return 0;
}
加载一个进程,通过路径+程序名来加载。
int execl(const char path, const char arg, ...);成功:无返回;失败:-1
对比execlp, 如加载“ls”命令带有-l,-F参数
execlp("ls", "ls", "-l", "-F", NULL); 使用程序名在PATH中搜索
execl("/bin/ls", "ls", "-l", "-F", NULL); 使用参数1给出的绝对路径搜索
加载一个进程,使用自定义环境变量env。
int execvp(const char file, const char argv[]);
变参形式:1、... 2、argv[] (main 函数也是变参函数,形式上等同于int main(int argc, char *argv0, ...))
变参终止条件:1、NULL结尾;2、固参指定。
execvp与execlp参数形式不同,原理一致。
char *argv[] = {"ls", "-l", "-a", NULL};
execvp("ls", argv);
execv("/bin/ls", argv);
练习:将当前系统中的进程信息,打印到文件中。
#include
#include
#include
#include
int main(int argc, char *argv[])
{
int fd;
fd = open("ps.out", O_WRONLY | O_CREAT | O_TRUNC, 0644);
if(fd < 0)
{
perror("open ps.out error");
exit(1);
}
dup2(fd, STDOUT_FILENO);
execlp("ps", "ps", "aux", NULL);//执行成功,后面的语句不会执行
perror("execlp error");
exit(1);
return 0;
}
exec函数一旦调用成功即执行新的程序,不返回。只有失败才返回,错误值-1。所以通常我们直接在exec函数调用后直接调用perror和exit(),无需if判断。
l(list) 命令行参数列表。
p(path) 搜索file时使用path变量
v(vector) 使用命令行参数数组
e(environment) 使用环境变量数组,不使用进程原有的环境变量,设置新加载程序运行的环境变量。
事实上,只有execve是真正的系统调用,其它五个函数最终都是调用execve,所以execve在man手册第2节,其它函数在man手册第3节。这些函数之间的关系如下图所示。
孤儿进程:父进程先于子进程结束,则子进程成为孤儿进程,子进程的父进程成为init进程,称为init进程领养孤儿进程。
示例,产生一个孤儿进程:
#include
#include
#include
int main(int argc, char * argv[])
{
pid_t pid;
pid = fork();
if(pid == 0)
{
while(1)
{
printf("I am child, my parent pid is %d\n", getppid());
sleep(1);
}
}
else if(pid >0)
{
printf("I am parent, my pid is %d \n", getpid());
sleep(9);
printf("----------parent going to die---------\n");
}
else
{
perror("fork");
return 1;
}
return 0;
}
僵尸进程:进程终止,父进程尚未回收,子进程残留资源(PCB)存放于内核中,变成僵尸(Zombie)进程。
特别注意:僵尸进程是不能使用kill命令清除掉的。因为kill命令只是用来终止进程的,而僵尸进程已经终止。
思考,用什么办法可清除僵尸进程呢?
示例,产生一个僵尸进程:
#include
#include
#include
#include
int main(int argc, char *argv[])
{
pid_t pid;
pid = fork();
if(pid == 0)
{
printf("---child, my parent=%d, going to sleep 10s \n", getppid());
sleep(10);
printf("-------------child die--------------\n");
}
else if(pid > 0)
{
while(1)
{
printf("I am parent, pid = %d, myson = %d \n", getpid(), pid);
}
}
else {
perror("fork error");
exit(1);
}
return 0;
}
一个进程在终止时会关闭所有文件描述符,释放在用户空间分配的内存,但它的PCB还保留着,内核在其中保存了一些信息:如果是正常终止则保存着退出状态,如果是异常终止则保存着导致该进程终止的信号是哪个。这个进程的父进程可以调用wait或waitpid获取这些信息,然后彻底清除掉这个进程。我们知道一个进程的退出状态可以在shell中用特殊变量$?查看,因为Shell是它的父进程,当它终止时Shell调用wait或waitpid得到它的退出状态,同时彻底清除掉这个进程。
可使用wait函数传出参数status来保存进程的退出状态。借助宏函数来进一步判断进程终止的具体原因。宏函数可以分为如下三组:
1、WIFEXITED(status) 为非0 --> 进程正常结束
WEXITSTATUS(status) 如上宏为真,使用此宏 --> 获取进程退出状态(exit的参数)
2、WIFSIGNALED(status) 为非0 --> 进程异常结束
WTERMSIG(status) 如上宏为真,使用此宏 --> 取得使进程终止的那个信号的编号。
3、WIFSTOPPED(status) 为非0 --> 进程处于暂停状态
WSTOPSIG(status) 如上宏为真,使用此宏 --> 取得使进程暂停的那个信号的编号。
WIFCONTINUED(status) 为真 --> 进程暂停后已经继续运行。
示例
#include
#include
#include
#include
int main(int argc, char *argv[])
{
pid_t pid, wpid;
int status;
pid = fork();
if(pid == 0)
{
printf("---child, my parent=%d, going to sleep 10s \n", getppid());
sleep(30);
printf("-------------child die--------------\n");
//exit(100);
return 100;
}
else if(pid > 0)
{
wpid = wait(&status);
if(wpid == -1)
{
perror("wait error");
exit(1);
}
if(WIFEXITED(status))
{
printf("child exit with %d \n", WEXITSTATUS(status));
}
if(WIFSIGNALED(status))
{
printf("child killed by %d \n", WTERMSIG(status));
}
while(1)
{
printf("I am parent, pid = %d, myson = %d \n", getpid(), pid);
}
}
else {
perror("fork error");
exit(1);
}
return 0;
}
作用同wait,但可指定pid进程清理,可以不阻塞。
pid_t waitpid(pid_t pid, int *status, int options);成功:返回清理掉的子进程ID;失败:-1(无子进程)。
特殊参数和返回情况:
参数pid:
>0 回收指定ID的子进程
-1 回收任意子进程(相当于wait)
0 回收和当前调用waitpid一个组的所有子进程
<-1 回收指定进程组内的任意子进程
参数status
参数options:
0 (wait)阻塞回收
WNOHANG 非阻塞回收(轮询)
返回:
成功 pid
失败 -1
0 参数3传WNOHANG,并且子进程尚未结束
注意:一次wait或waitpid调用只能清理一个子进程,清理多个子进程应使用循环。
作业:父进程fork 3个子进程,三个子进程一个调用ps命令,一个调用自定义程序1(正常),一个调用自定义程序2(会出现错误)。父进程使用waitpid对其子进程进行回收。
示例
#include
#include
#include
#include
int main(int argc, char *argv[])
{
int n = 5, i;//默认创建五个子进程
pid_t p, q;
pid_t wpid;
if(argc == 2)
{
n = atoi(argv[1]);
}
for(i = 0; i < n; i++)
{//出口1,父进程专用出口
p = fork();
if(p == 0)
{
break;//出口2,子进程出口,i不自增
}
else if (i == 3)
{
q = p;
}
}
if(n == i)
{
sleep(n);
printf("I am parent, pid = %d\n", getpid(), getgid());
//waitpid(q, NULL, 0); //1、回收第三个子进程
//while(waitpid(-1, NULL, 0)); //2、等价于wait(NULl),阻塞回收任意子进程
do
{
//3、非阻塞回收任意子进程
//如果wpid == 0 说明子进程正在运行
wpid = waitpid(-1, NULL, WNOHANG);
if(wpid > 0)
{
n--;
}
sleep(1);
}
while(n > 0)
printf("wait finish\n");
}
else
{
sleep(i);
printf("I'm %dth child, pid = %d, gid = %d \n", i+1, getpid(), getgid());
}
return 0;
}
管理的原理:管道实为内核使用环形队列机制,借助内核缓冲区(4k)实现。
int pipe(int pipefd[2]);
父子进程间通信ls | wc -l
#include
#include
#include
int main(void)
{
pid_t pid;
int fd[2];
pipe(fd);
pid = fork();
//子进程
if(pid == 0){
//子进程从管道中读数据,关闭写端
close(fd[1]);
//让wc从管道中读取数据
dup2(fd[0], STDIN_FILENO);
//wc命令默认从标准读入取数据
execlp("wc", "wc", "-l", NULL);
}else {
//父进程向管道中写数据,关闭读端
close(fd[0]);
//将ls的结果写入管道中
dup2(fd[1], STDOUT_FILENO);
//ls输出结果默认对应屏幕
execlp("ls", "ls", NULL);
}
return 0;
}
兄弟进程间通信
#include
#include
#include
int main(void)
{
pid_t pid;
int fd[2], i;
pipe(fd);
for(i = 0; i < 2; i++){
if((pid = fork()) == 0){
break;
}
}
if(i == 0){ //兄
close(fd[0]); //写,关闭读端
dup2(fd[1], STDOUT_FILENO);
execlp("ls", "ls", NULL);
}else if(i == 1){ //弟
close(fd[1]); //读,关闭写端
dup2(fd[0], STDIN_FILENO);
execlp("wc", "wc", "-l", NULL);
}else {
close(fd[0]);
close(fd[1]);
for(i = 0; i < 2; i++){ //两个儿子wait两次
wait(NULL);
}
}
return 0;
}
命令:ulimit -a
函数:fpathconf
, 参数2:__PC_PIPE_BUF
命名管道(Linux基础文件类型)
mkfifo
int mkfifo(const char *pathname, mode_t mode);
使用文件也可以完成IPC,理论依据是,fork后,父子进程共享文件描述符。也就共享打开的文件。
练习:编程测试,父子进程共享打开的文件。借助文件进行进程间通信。
思考:无血缘关系的进程可以打开同一个文件进行通信吗?为什么?
示例
/**
*父子进程共享打开的文件描述符------使用文件完成进程间共享
*/
#include
#include
#include
#include
#include
#include
int main(int argc, char *argv[])
{
int fd1, fd2;
pid_t pid;
char * str = "----test for shared fd in parent child process----\n";
pid = fork();
if(pid < 0)
{
perror("fork error");
exit(1);
}
else if(pid == 0)
{
fd1 = open("test.txt", O_RDWR);
if(fd1 < 0)
{
perror("open error");
exit(1);
}
//子进程写入数据
write(fd1, str, strlen(str));
printf("child wrote over...\n");
}
else
{
fd2 = open("test.txt", O_RDWR);
if(fd2 < 0)
{
perror("open error");
exit(1);
}
sleep(1); //保证子进程写入数据
//父进程读取数据
int len = read(fd2, buf, sizeof(buf));
write(STDOUT_FILENO, buf, len);
wait(NULL);
}
return 0;
}
存储映射I/O(Memory-mmapped I/O)使一个磁盘文件与存储空间中一个缓冲区相映射。于是当从缓冲区取数据,就相当于读文件中的相应字节。于此类似,将数据存入缓冲区,则相应的字节就自动写入文件。这样,就可在不适用read和write函数的情况下,使用地址(指针)完成I/O操作。
使用这种方法,首先应通知内核,将一个指定文件映射到存储区域中。这个映射工作可以通过mmap函数来实现。
mmap函数
void mmap(void addr, size_t length, int prot, int flags, int fd, off_t offset);
示例
#include
#include
#include
#include
#include
#include
void sys_err(char *str)
{
perror(str);
exit(1);
}
int main(int argc, char *argv[])
{
char *p = NULL;
int fd = open("test.txt", O_CREAT|O_REWR, 0644);
if(fd < 0)
{
sys_err("open error");
}
int len = ftruncate(fd, 4);
if(len == -1)
{
sys_err("ftruncate error");
}
p = mmap(NULL, 4, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
if(p == MAP_FAILED)
{
sys_err("mmap error");
}
strcpy(p, "abc"); //写数据
int ret = munmap(p, 4);
if(ret == -1)
{
sys_err("munmap error");
}
close(fd);
return 0;
}
munmap函数
同malloc函数申请内存空间类似的,mmap建立的映射区在使用结束后也应调用类似free的函数来释放。
int munmap(void *addr, size_t length);
借鉴malloc和free函数原型,尝试封装自定义smalloc,sfree来完成映射区的建立和释放。思考函数应如何设计?
mmap注意事项
练习:父进程创建映射区,然后fork子进程,子进程修改映射区内容,然后,父进程读取映射区内容,查验是否共享。
#include
#include
#include
#include
#include
#include
int var = 100;
int main(int argc, char *argv[])
{
int *p;
pid_t pid;
int fd;
fd = open("temp", O_RDWR|O_CREAT|O_TRUNC, 0644);
if(fd < 0)
{
perror("open error");
exit(1);
}
unlink("temp"); //删除临时文件目录项,使之具备被释放条件
ftruncate(fd, 4);
p = (int *)mmap(NULL, 4, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
//p = (int *)mmap(NULL, 4, PROT_READ|PROT_WRITE, MAP_PRIVATE, fd, 0);
if(p == MAP_FAILED) //注意:不是p == NULL
{
perror("mmap error");
exit(1)
}
close(fd); //映射区建立完毕,即可关闭文件
pid = fork();//创建子进程
if(pid == 0)
{
*p = 2000;
var = 1000;
printf("child, *p = %d, var = %d\n", *p, var);
}
else
{
sleep(1);
printf("parent, *p = %d, var = %d\n", *p, var);
wait(NULL);
}
int ret = mnumap(p, 4);//释放映射区
if(ret == -1)
{
perror("mnumap error");
exit(1);
}
return 0;
}
使用MAP_ANONYMOUS(或MAP_ANON),如:
int *p = mmap(NUll, 4, PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANONYMOUS, -1, 0);
"4"随意举例,该位置大小,可依实际需要填写。
需要注意的是,MAP_ANONYMOUS和MAP_ANON这两个宏是Linux操作系统特有的宏。在类Unix系统中如无该宏定义,可使用如下两步来完成匿名映射区的建立。
1、fd = open("/dev/zero", O_RDWR);
2、p = mmap(NULL, size, PROT_READ|PROT_WRITE, MMAP_SHARED, fd, 0);
示例
#include
#include
#include
#include
#include
#include
int var = 100;
int main(int argc, char *argv[])
{
int *p;
pid_t pid;
p = (int *)mmap(NULL, 4, PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANON, -1, 0);
if(p == MAP_FAILED) //注意:不是p == NULL
{
perror("mmap error");
exit(1)
}
pid = fork();//创建子进程
if(pid == 0)
{
*p = 2000;
var = 1000;
printf("child, *p = %d, var = %d\n", *p, var);
}
else
{
sleep(1);
printf("parent, *p = %d, var = %d\n", *p, var);
wait(NULL);
}
int ret = mnumap(p, 4);//释放映射区
if(ret == -1)
{
perror("mnumap error");
exit(1);
}
return 0;
}
实质上mmap是内核借助文件帮我们创建了一个映射区,多个进程之间利用该映射区完成数据传递。由于内核空间多进程共享,因此无血缘关系的进程间也可以使用mmap来完成通信。只要设置相应的标志位参数flags即可。若想实现共享,当然应该使用MAP_SHARED了。
读端
#include
#include
#include
#include
#include
#include
#include
struct STU
{
int id;
char name[20];
char sex;
}
void sys_err(char *str)
{
perror(str);
exit(-1);
}
int main(int argc, char *argv[])
{
int fd;
struct STU student;
struct STU *mm;
if(argc < 2)
{
printf("./a.out file_shared\n");
exit(-1);
}
fd = open(argv[1], O_RDONLY);
if(fd == -1)
{
sys_err("open error");
}
mm = mmap(NULL, sizeof(student), PROT_READ, MAP_SHARED, fd, 0);
if(mm == MAP_FAILED)
{
sys_err("mmap error");
}
close(fd);
while(1)
{
printf("id=%d\t name=%s\t %c\n", mm->id, mm->name, mm->sex);
sleep(2);
}
munmap(mm, sizeof(student));
return 0;
}
写端
#include
#include
#include
#include
#include
#include
#include
#include
struct STU
{
int id;
char name[20];
char sex;
}
void sys_err(char *str)
{
perror(str);
exit(-1);
}
int main(int argc, char *argv[])
{
int fd;
struct STU student = {10, "xiaoming", 'm'};
char *mm;
if(argc < 2)
{
printf("./a.out file_shared\n");
exit(-1);
}
fd = open(argv[1], O_RDWR | O_CREAT, 0664);
ftruncate(fd, sizeof(student));
mm = mmap(NULL, sizeof(student), PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if(mm == MAP_FAILED)
{
sys_err("mmap error");
}
close(fd);
while(1)
{
memcpy(mm, &student, sizeof(student));
student.id++;
sleep(1);
}
munmap(mm, sizeof(student));
return 0;
}
你的程序应该可以处理以下命令:
ls -l -R > file1
cat < file1 | wc -c > file1
可以使用kill -l
命令来查看当前系统可使用的信号有哪些。
1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP
6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1
11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM
16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ
26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR
31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3
38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8
43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7
58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2
63) SIGRTMAX-1 64) SIGRTMAX
不存在编号为0的信号。其中1-31号信号称之为常规信号(也叫普通信号或标准信号),34-64称之为实时信号,驱动编程与硬件相关。名字上区别不大。而前32个名字各不相同。
可通过man 7 signal
查看帮助文档获取。也可查看/usr/src/linux-headers-3.16.0-30/arch/s390/include/uapi/asm/signal.h
Signal Value Action Comment
──────────────────────────────────────────────────────────────────────
SIGHUP 1 Term Hangup detected on controlling terminal or death of controlling process
SIGINT 2 Term Interrupt from keyboard
SIGQUIT 3 Core Quit from keyboard
SIGILL 4 Core Illegal Instruction
SIGABRT 6 Core Abort signal from abort(3)
SIGFPE 8 Core Floating point exception
SIGKILL 9 Term Kill signal
SIGSEGV 11 Core Invalid memory reference
SIGPIPE 13 Term Broken pipe: write to pipe with no readers
SIGALRM 14 Term Timer signal from alarm(2)
SIGTERM 15 Term Termination signal
SIGUSR1 30,10,16 Term User-defined signal 1
SIGUSR2 31,12,17 Term User-defined signal 2
SIGCHLD 20,17,18 Ign Child stopped or terminated
SIGCONT 19,18,25 Cont Continue if stopped
SIGSTOP 17,19,23 Stop Stop process
SIGTSTP 18,20,24 Stop Stop typed at terminal
SIGTTIN 21,21,26 Stop Terminal input for background process
SIGTTOU 22,22,27 Stop Terminal output for background process
The signals SIGKILL and SIGSTOP cannot be caught, blocked, or ignored.
man 7 signal
帮助文档中可看到:The signals SIGKILL and SIGSTOP cannot be caught, blocked, or ignored.
这里特别强调了9) SIGKILL
和19) SIGSTOP
信号,不允许忽略和捕捉,只能执行默认动作。甚至不能将其设置为阻塞。另外需清楚,只有每个信号所对应的事件发生了,该信号才会被递达(但不一定递达),不应乱发信号!!
ctrl+c
组合键时,用户终端向正在运行中的由该终端启动的程序发出此信号。默认动作为终止进程。ctrl+\
组合键时产生该信号,用户终端向正在运行中的由该终端启动的程序发出此信号。默认动作为终止进程。ctrl+z
组合键时发出这个信号。默认动作为暂停进程。 ctrl+c 2) SIGINT(终止/中断) "INT" -- Interrupt
ctrl+z 20)SIGTSTP(暂停/停止) "T" -- Terminal终端
ctrl+\ 3) SIGQUIT(退出)
除0操作 8) SIGFPE(浮点数例外) "F" -- float 浮点数
非法访问内存 11)SIGSEGV(段错误)
总线错误 7) SIGBUS
kill -SIGKILL pid
int kill(pid_t pid, int sig);
示例
#include
#include
#include
#include
#define N 5
int main(int argc, char *argv[])
{
int i;
pid_t pid;
for(i = 0; i < N; i++)
{
pid = fork();
if(pid == 0)
break;
if(i == 2)
q = pid;
}
if(i < N)
{
while(1)
{
printf("I am child %d, getpid() = %u \n", i+1, getpid());
sleep(1);
}
}
else
{
sleep(1);
kill(q, SIGKILL);
while(1);
}
// int ret = kill(getpid(), SIGKILL);
// if(ret == -1)
// exit(1);
return 0;
}
raise(signo) == kill(getpid(), signo)
int raise(int sig);
void abort(void);
该函数无返回unsigned int alarm(unsigned int seconds);
练习:编写程序,测试你使用的计算机1秒钟能数多少个数。
#include
#include
int main(void)
{
int i;
alarm(1);
for(i = 0; ; i++)
{
printf("%d\n", i);
}
return 0;
}
使用time命令查看程序执行的时间。程序运行的瓶颈在于IO,优化程序,首选优化IO。
time ./alarm > out
实际执行时间 = 系统时间 + 用户时间 + 等待时间
int setitimer(int which, const struct itimerval *new_value, struct itimerval *old_value);
which
:指定定时方式
ITIMER_REAL --> 14)SIGLARM
,计算自然时间。ITIMER_VIRTUAL --> 26)SIGVTALRM
,只计算进程占用CPU时间。ITIMER_PROF --> 27)SIGPROF
,计算占用CPU及执行系统调用的时间。练习:使用setitimer函数实现alarm函数,重复计算机1秒数数程序。
#include
#include
#include
#include
// struct itimerval {
// struct timeval it_interval; /* Interval for periodic timer */
// struct timeval it_value; /* Time until next expiration */
// };
// struct timeval {
// time_t tv_sec; /* seconds */
// suseconds_t tv_usec; /* microseconds */
// };
unsigned int my_alarm(unsigned int sec)
{
struct itimerval it, oldit;
int ret;
it.it_value.tv_sec = sec;
it.it_value.tv_usec = 0;
it.it_interval.tv_sec = 0;
it.it_interval.tv_usec = 0;
ret = setitimer(ITIMER_REAL, &it, &oldit);
if(ret == -1)
{
perror("setitimer");
exit(1);
}
return oldit.it_value.tv_sec;
}
int main(void)
{
int i;
my_alarm(1);
for(i = 0; ; i++)
{
printf("%d\n", i);
}
return 0;
}
man page
编写程序,测试it_interval、it_value这两个参数的作用。
示例
#include
#include
#include
void myfunc(int signo)
{
printf("hello\n");
}
int main(void)
{
struct itimerval it, oldit;
signal(SIGALRM, myfunc); //注册SIGALRM信号的捕捉处理函数
it.it_value.tv_sec = 5;
it.it_value.tv_usec = 0;
it.it_interval.tv_sec = 3;
it.it_interval.tv_usec = 0;
if(setitimer(ITIMER_REAL, &it, &oldit) == -1)
{
perror("setitimer error");
return -1;
}
while(1);
return 0;
}
sigset_t set; //typedef unsigned long sigset_t;
int sigemptyset(sigset_t *set); 将某个信号集清0 成功:0;失败:-1
int sigfillset(sigset_t *set); 将某个信号集置1 成功:0;失败:-1
int sigaddset(sigset_t *set, int signum); 将某个信号加入信号集 成功:0;失败:-1
int sigdelset(sigset_t *set, int signum); 交某个信号清出信号集 成功:0;失败:-1
int sigismember(const sigset_t *set, int signum); 判断某个信号是否在信号集中 不在:0;在:1;出错:-1
sigset_t类型的本质是位图。但不应该直接使用位操作,而应用使用上述函数,保证跨系统操作有效。
对比认知select函数。
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
mask = mask | set
mask = mask & ~set
mask = set
,若调用sigprocmask解除了对当前若干信号的阻塞,则在sigprocmask返回前,至少将其中一个信号递达。int sigpending(sigset_t *set);
练习:编写程序。把所有常规信号的未决状态打印至屏幕。
#include
#include
#include
void printped(sigset_t *ped)
{
int i;
for(i = 1; i < 32; i++)
{
if(sigismember(ped, i) == 1)
{
putchar('1');
}
else
{
putchar('0');
}
}
printf("\n");
}
int main(void)
{
sigset_t myset, oldset, ped;
sigemptyset($myset);
sigaddset(&myset, SIGQUIT);
sigaddset(&myset, SIGINT);
sigaddset(&myset, SIGTSTP);
sigaddset(&myset, SIGSEGV);
sigaddset(&myset, SIGKILL); //9,19不能屏蔽,加入也没用
sigprocmask(SIG_BLOCK, &myset, &oldset);
while(1)
{
sigpending(&ped);
printped(&ped);
sleep(1);
}
return 0;
}
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
void (*signal(int signum, void (*sighandler_t)(int)))(int);
示例
#include
#include
#include
#include
typedef void (*sighandler_t)(int);
void catchsigiint(int signo)
{
printf("-----SIGINIT-----\N");
}
int main(void)
{
sighandler_t handler;
handler = signal(SIGINT, catchsigiint);
if(handler == SIG_ERR)
{
perror("signal error");
exit(1);
}
while(1);
return 0;
}
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
struct sigaction
struct sigaction {
void (*sa_handler)(int);
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void);
};
sa_restorer:该元素是过时的,不应该使用,POSIX.1标准将不指定该元素。(弃用)
sa_sigaction:当sa_flags被指定为SA_SIGINFO标志时,使用该信号处理程序。(很少使用)
重点掌握:
1、sa_handler:指定信号捕捉后和处理函数名(即注册函数)。也可赋值为SIG_IGN表忽略或SIG_DFL表执行默认动作。
2、sa_mask:调用信号处理函数时,所要屏蔽的信号集(信号屏蔽字)。注意:仅在修理函数被调用期间屏蔽生效,是临时性设置。
3、sa_flags:通常设置为0,表使用默认属性。
示例
#include
#include
#include
#include
void docatch(int signo)
{
printf("%d signal is catched\n", signo);
}
int main(void)
{
int ret;
struct sigaction act;
act.sa_handler = docatch;
sigemptyset(&act.sa_mask);
sigaddset(&act.sa_mask, SIGQUIT);
act.sa_flags = 0; //默认属性。信号捕捉函数执行期间,自动屏蔽本信号
ret = sigaction(SIGINT, &act, NULL);
if(ret < 0)
{
perror("sigaction error");
exit(1);
}
while(1);
return 0;
}
int pause(void);
,返回值:-1,并设置errno为EINTR。实现示例:
#include
#include
#include
#include
#include
void catch_signalrm(int signo)
{
printf("%d signal is catched.", signo);
}
unsigned int mysleep(unsigned int seconds)
{
int ret;f
struct sigaction act, oldact;
act.sa_handler = catch_signalrm;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
ret = sigaction(SIGALRLM, &act, &oldact);
if(ret == -1)
{
perror("sigaction error");
exit(1);
}
alarm(seconds);
ret = pause(); // 主动挂起,等信号
if(ret == -1 && errno == EINTR)
{
printf("pause sucess\n");
}
ret = alarm(0); //闹铃清零
sigaction(SIGALRM, &oldact, NULL); //恢复SIGALRM信号旧有的处理方式。
return ret;
}
int main(void)
{
while(1)
{
mysleep(3);
printf("-------------------\n");
}
return 0;
}
int sigsuspend(const sigset_t *mask);
,挂起等待信号。改进版mysleep
#include
#include
#include
#include
#include
void sig_alrm(int signo)
{
printf("%d signal is catched.", signo);
}
unsigned int mysleep(unsigned int seconds)
{
int ret;f
struct sigaction newact, oldact;
sigset_t newmask, oldmask, suspmask;
//1、为SIGALRM设置捕捉函数,一个空函数
newact.sa_handler = sig_alrm;
sigemptyset(&newact.sa_mask);
newact.sa_flags = 0;
sigaction(SIGALRM, &newact, &oldact);
//2、设置阻塞信号集,阻塞SIGALRM信号
sigemptyset(&newmask);
sigaddset(&newmask, SIGALRM);
sigprocmask(SIG_BLOCK, &newmask, &oldmask);
//3、定时n秒,到时后可以产生SIGALRM信号
alarm(seconds);
//4、构造一个调用sigsuspend临时有效的阻塞信号集,在临时阻塞信号集里解除SIGALRM的阻塞
suspmask = oldmask;
sigdelset(&suspmask, SIGALRM);
//5、sigsuspend调用期间,采用临时阻塞信号集suspmask替换原有阻塞信号集
//这个信号集中不包含SIGALR信号,同时挂起等待
//当sigsuspend被信号唤醒返回时,恢复原有的阻塞信号集
sigsuspend(&suspmasks);
unslept = alarm(0);
//6、恢复SIGALRM原有的处理动作
sigaction(SIGALRM, &oldact, NULL);
//7、解除对SIGALRM的阻塞
sigprocmask(SIG_SETMASK, &oldmask, NULL);
return (unslept);
}
int main(void)
{
while(1)
{
mysleep(3);
printf("-------------------\n");
}
return 0;
}
分析如下父子进程交替的数数程序。当捕捉函数里面的sleep取消,程序即会出现问题。请分析原因。
#include
#include
#include
#include
int n = 0; flag = 0;
void sys_err(char *str)
{
perror(str);
exit(1);
}
void do_sig_child(int num)
{
printf("I am child %d\t%d\n", getpid(), n);
n+=2;
flag = 1;
//sleep(1);
}
void do_sig_parent(int num)
{
printf("I am parent %d\t%d\n", getpid(), n);
n+=2;
flag = 1;//数数完成
//sleep(1);
}
int main(void)
{
pid_t pid;
struct sigaction act;
if((pid = fork()) < 0)
{
sys_err("fork");
}
else if(pid > 0 )
{
n = 1;
sleep(1);
act.sa_handler = do_sig_parent;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
sigaction(SIGUSR2, &act, NULL); //注册自己的信号捕捉函数
do_sig_parent(0);
while(1)
{
// wait for signal
if(flag == 1)//父进程数数完成
{
kill(pid, SIGUSR1);
flag = 0;//标志已经给子进程发送完信号
}
}
}
else if(pid == 0)
{
n = 2;
act.sa_handler = do_sig_child;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
sigaction(SIGUSR1, &act, NULL);//父进程数数完成发送SIGUSR1给子进程。
while(1)
{
// wait for signal
if(flag == 1)
{
kill(getppid(), SIGUSR2);
flag = 0;
}
}
}
}
现阶段,我们在编程期间如若使用全局变量,应在主观上注意全局变量的异步IO可能造成的问题。
显示,insert函数是不可重入函数,重入调用,会导致意外结果呈现。究其原因,是该函数内部实现使用了全局变量。
子进程结束运行,其父进程会收到SIGCHLD信号。该信号的默认处理动作是忽略。可以捕捉该信号,在捕捉函数中完成子进程状态的回收。
#include
#include
#include
#include
#include
#include
#include
void sys_err(str)
{
perror(str);
exit(1);
}
void do_sig_child(int signo)
{
int status;
pid_t pid;
while((pid = waitpid(0, &status, WNOHANG)) > 0){
if(WIFEXITED(status))
printf("-----------child %d exit %d \n", pid, WEXITSTATUS(status));
else if(WIFSIGNALED(status))
printf("child %d cancel signal %d \n", pid, WTERMSIG(status));
}
}
int main(void)
{
pid_t pid;
int i;
//阻塞SIGCHLD
for(i = 0; i < 10; i++){
if((pid = fork()) == 0)
break;
else if(pid < 0)
sys_err("fork");
}
if(pid == 0){ //10个子进程
int n = 1;
while(n--){
printf("child ID %d \n", getpid());
sleep(1);
}
return i+1;
}else if(pid > 0){
//SIGCHLD阻塞
struct sigaction act;
act.sa_handler = do_sig_child;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
sigaction(SIGCHLD, &act, NULL);
//解除对SIGCHLD的阻塞
while(1){
printf("Parent ID %d \n", getpid());
sleep(1);
}
}
return 0;
}
思考:信号不支持排队,当正在执行SIGCHLD捕捉函数时,再过来一个或多个SIGCHLD信号怎么办?
sigqueue函数对应kill函数,但可在向指定进程发送信号的同时携带参数
int sigqueue(pid_t pid, int sig, const union sigval value);
成功:0
失败:-1,设置errno
union sigval{
int sival_int;
void *sival_ptr;
}
向指定进程发送指定信号的同时,携带数据。但,如传地址,需注意,不同进程之间虚拟地址空间各自独立,将当前进程地址传递给另一进程没有实际意义。
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
struct sigaction {
void (*sa_handler)(int);
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void);
};
SSH、Telnet...,网络终端。
简单来说,一个Linux系统启动,大致经历如下步骤:
init --> fork --> exec --> getty --> 用户输入帐号 --> login --> 输入密码 --> exec --> bash
硬件驱动程序负责读写实际的硬件设备,比如从键盘读入字符和把字符输出到显示器,线路规程像一个过滤器,对于某些特殊字符并不是让它直接通过,而是做特殊处理,比如在键盘上按下ctrl+z,对应的字符并不会被用户程序的read读到,而是被线程规程截获,解释成SIGTSTP信号发给前台进程,通常会使该进程停止。线路规程应该过滤哪些字符和做哪些特殊处理是可以配置的。
下面我们借助ttyname函数,通过实验看一下各种不同的终端所对应的设备文件名。
#include
#include
int main(void)
{
printf("fd 0: %s\n", ttyname(0));
printf("fd 1: %s\n", ttyname(1));
printf("fd 2: %s\n", ttyname(2));
return 0;
}
kill -SIGKILL -进程组ID(负的)
来将整个进程组内的进程全部杀死。pid_t getpgrp(void);
pid_t getpgid(pid_t pid);
示例
#include
#include
#include
int main(void)
{
pid_t pid;
if((pid = fork()) < 0){
perror("fork");
exit(1);
}else if(pid == 0){
printf("child PID == %d\n", getpid());
printf("child Group ID == %d\n", getpgid(0)); //返回组ID
//printf("child Group ID == %d\n", getpgrp()); //返回组ID
sleep(7);
printf("----Group ID of child is changed to %d\n", getpgid(0));
exit(0);
}else if(pid > 0){
sleep(1);
setpgid(pid, pid);//让子进程自立门户,成为进程组组长,以它的pid为进程组id
sleep(13);
printf("\n");
printf("parent PID == %d\n", getpid());
printf("parent's parent process PID == %d\n", getppid());
printf("parent Group ID == %d\n", getpgid(0));
sleep(5);
setpgid(getpid(), getppid());//改变父进程的组ID为父进程的父进程
printf("\n -----Group ID of parent is changed to %d \n", getpgid(0));
while(1);
}
return 0;
}
练习:fork一个子进程,并使其创建一个新会话。查看进程组ID、会话ID前后变化。
#include
#include
#include
int main(void)
{
pid_t pid;
if((pid = fork()) < 0){
perror("fork error");
exit(1);
} else if(pid == 0){
printf("child process PID is %d\n", getpid());
printf("Group ID of child is %d\n", getpgid(0));
printf("Session ID of child is %d\n", getsid(0));
sleep(0);
setsid();//子进程非组长进程,故其成为新会话首进程,且成为组长进程。该进程组ID即为会话进程
printf("Changed:\n");
printf("child process PID is %d\n", getpid());
printf("Group ID of child is %d\n", getpgid(0));
printf("Session ID of child is %d\n", getsid(0));
sleep(20);
exit(0);
}
return 0;
}
示例
#include
#include
#include
#include
#include
#include
int main(void)
{
pid_t pid, sid;
int ret;
pid = fork();
if(pid > 0)
{
exit(1);
}
sid = setsid();
ret = chdir("/home/super/");
if(ret == -1)
{
perror("chdir error");
exit(1);
}
umask(0022);
close(STDIN_FILENO);
open("/dev/null", O_RDWR);
dup2(0, STDOUT_FILENO);
dup2(0, STDERR_FILENO);
while(1);
return 0;
}