【学习点滴】linux笔记,fork底层实现、信号、进程、vi

目录

Vi三种模式详解

进程

父子进程究竟共享啥,复制了啥:

1.fork()函数

fork底层实现

wait:

exec函数族:可让子进程做其他事

进程间通信:

信号:

      信号的发送

kill()

  sigqueue()

  alarm()

  setitimer()

 abort()

 raise()

信号的捕捉

 signal()

sigaction()

 信号集及信号集操作函数:

信号阻塞与信号未决:


linux下安装gcc和g++

su 到root用户下,输入命令

yum install gcc 即可安装gcc,然后再

yum install gcc-c++ 即可安装g++

 

第一次在linux下编译运行c++程序:

用vim编辑器新建一个cpp文件:vim helloworld.cpp

此文件中写好c++程序,然后:wq退出

在终端中输入  g++ -o main helloworld.cpp -lm 即可编译cpp程序,并将生成的程序命名为main

若编译成功,则不会返回任何信息

然后输入./main来调用刚刚生成的文件,效果:

 

 

Vi三种模式详解

命令行模式 (command mode/一般模式)
  任何时候,不管用户处于何种模式,只要按一下“ESC”键,即可使Vi进入命令行模式;我们在shell环境(提示符为$)下输入启动Vi命令,进入编辑器时,也是处于该模式下。 
  在该模式下,用户可以输入各种合法的Vi命令,用于管理自己的文档。此时从键盘上输入的任何字符都被当做编辑命令来解释,若输入的字符是合法的Vi命令,则Vi在接受用户命令之后完成相应的动作。但需注意的是,所输入的命令并不在屏幕上显示出来。若输入的字符不是Vi的合法命令,Vi会响铃报警。
 
文本输入模式 (input mode/编辑模式)
  在命令模式下输入插入命令i(insert)、附加命令a (append)、打开命令o(open)、修改命令c(change)、取代命令r或替换命令s都可以进入文本输入模式。在该模式下,用户输入的任何字符都被Vi当做文件内容保存起来,并将其显示在屏幕上。在文本输入过程中,若想回到命令模式下,按"ESC"键即可。 

末行模式 (last line mode/指令列命令模式)
  末行模式也称ex转义模式。 
  Vi和Ex编辑器的功能是相同的,二者主要区别是用户界面。在Vi中,命令通常是单个键,例如i、a、o等;而在Ex中,命令是以按回车键结束的正文行。Vi有一个专门的“转义”命令,可访问很多面向行的Ex命令。在命令模式下,用户按“:”键即可进入末行模式下,此时Vi会在显示窗口的最后一行(通常也是屏幕的最后一行)显示一个“:”作为末行模式的提示符,等待用户输入命令。多数文件管理命令都是在此模式下执行的(如把编辑缓冲区的内容写到文件中等)。末行命令执行完后,Vi自动回到命令模式。

 

 

dd命令用于复制文件并对原文件的内容进行转换和格式化处理。dd命令功能很强大的,对于一些比较底层的问题,使用dd命令往往可以得到出人意料的效果。用的比较多的还是用dd来备份裸设备。但是不推荐,如果需要备份oracle裸设备,可以使用rman备份,或使用第三方软件备份,使用dd的话,管理起来不太方便。 

 

df命令用于显示磁盘分区上的可使用的磁盘空间。默认显示单位为KB。可以利用该命令来获取硬盘被占用了多少空间,目前还剩下多少空间等信息。 
 

top命令可以实时动态地查看系统的整体运行情况,是一个综合了多方信息监测系统性能和运行信息的实用工具。通过top命令所提供的互动式界面,用热键可以管理。 

netstat命令用来打印Linux中网络系统的状态信息,可让你得知整个Linux系统的网络情况。 

 

du查看目录大小,df查看磁盘使用情况。
我常使用的命令(必要时,sudo使用root权限),
1).查看某个目录的大小:du -hs /home/master/documents
查看目录下所有目录的大小并按大小降序排列:sudo du -sm /etc/* | sort -nr | less
2).查看磁盘使用情况(文件系统的使用情况):sudo df -h
df --block-size=GB
-h是使输出结果更易于人类阅读;du -s只展示目录的使用总量(不分别展示各个子目录情况),-m是以 
MB为单位展示目录的大小(当然-k/-g就是KB/GB了)。

3,du使用详细案例
a:显示全部目录和其次目录下的每个档案所占的磁盘空间
s:只显示各档案大小的总合 
b:大小用bytes来表示
x:跳过在不同文件系统上的目录不予统计
a:递归地显示指定目录中各文件及子孙目录中各文件占用的数据块数
...

 

 

进程

 

父子进程究竟共享啥,复制了啥:

#include <stdio.h>
#include <stdlib.h>
#include < unistd.h>
int global = 1; //初始化的全局变量,存在data段
int main(){
	pid_t pid;
	int stack =1; //局部变量,存在栈
	int *heap;	//指向堆变量的指针
	heap=(int *)malloc(sizeof(int));
	*heap=3;
	pid=fork();
	if(pid<0){
		perror("fail to fork");
		exit(-1);
	}
	else if(pid==0){
		//子进程, 改变变量的值
		global++;
		stack++;
		(*heap)++;
		printf("in sub-process, global:%d, stack:%d, *heap:%d \n", global,stack,*heap);
		exit(0);
	}
	else{
		sleep(2);	//休眠2 秒钟,确保子进程已执行完毕, 再执行父进程
		printf("in parent-process, global:%d, stack:%d, *heap:%d \n", global,stack,*heap);
	}
	
	return 0;
}

执行结果:

事实上,子进程完全复制了父进程的地址空间的内容,包括堆栈段和数据段的内容。但是,子进程并没有复制代码段,而是和父进程共用代码段。这样做是合理的,因为子进程可能执行不同的流程来改变数据段和堆栈段,因此需要分开存储父子进程各自的数据段和堆栈段。但是代码段是只读的,不存在被修改的问题,因此代码段可以让父子进程共享,以节省存储空间。父进程和子进程共享fork之后的程序段、打开的文件、工作目录、共享内存,注意锁不继承。

 

 

1.fork()函数

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>

int main(void)
{
    pid_t pid;
    printf("xxxxxxxxxxxxxxx\n");
    
    pid = fork();
    if(pid == -1){
        perror("fork error:");
        exit(1);
    }
    else if(pid==0)
        printf("i'm child ,pid = %u,ppid=%u \n",get_pid(),get_ppid());
    else{
        printf("i'm parent,pid = %u,ppid=%u \n",get_pid(),get_ppid());
        sleep(1);
    }

    printf("yyyyyyyyyyyyyyy\n");
    return 0;

}

循环创建n个子进程:


#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>

int main(void)
{
    int i;
    pid_t pid;
    printf("xxxxxxxxxxxxxxx\n");
    
    for(i=0;i<5;i++){
    pid = fork();
    if(pid == 0){
        break;
    }
    if(i<5)
        printf("i'm child ,pid = %u,ppid=%u \n",get_pid(),get_ppid());
    else{
        printf("i'm parent,pid = %u,ppid=%u \n",get_pid(),get_ppid());
        sleep(i);
    }

    printf("yyyyyyyyyyyyyyy\n");
    return 0;

}
 

2.fork()、vfork()、clone()的区别

进程的四要素:

(1)有一段程序供其执行(不一定是一个进程所专有的),就像一场戏必须有自己的剧本。

(2)有自己的专用系统堆栈空间(私有财产)

(3)有进程控制块(task_struct)(“有身份证,PID”)

(4)有独立的存储空间。

缺少第四条的称为线程,如果完全没有用户空间称为内核线程,共享用户空间的称为用户线程。

一、fork()

fork()函数调用成功:返回两个值; 父进程:返回子进程的PID;子进程:返回0;

失败:返回-1;

 

fork 创造的子进程复制了父亲进程的资源(写时复制技术),包括内存的内容task_struct内容(2个进程的pid不同)。这里是资源的复制不是指针的复制。

读时共享写时复制(Copy-On-Write):在fork()之后exec之前两个进程用的是相同的物理空间(内存区),先把页表映射关系建立起来,并不真正将内存拷贝。子进程的代码段、数据段、堆栈都是指向父进程的物理空间,也就是说,两者的虚拟空间不同,但其对应的物理空间是同一个。当父进程中有更改相应段的行为发生时,如进程写访问,再为子进程相应的段分配物理空间,如果不是因为exec,内核会给子进程的数据段、堆栈段分配相应的物理空间(至此两者有各自的进程空间,互不影响),而代码段继续共享父进程的物理空间(两者的代码完全相同)。而如果是因为exec,由于两者执行的代码不同,子进程的代码段也会分配单独的物理空间。fork时子进程获得父进程数据空间、堆和栈的复制所以变量的地址(当然是虚拟地址)是一样的。

二、vfork()

vfork是一个过时的应用,vfork也是创建一个子进程,但是子进程共享父进程的空间。在vfork创建子进程之后,父进程阻塞,直到子进程执行了exec()或者exit()。vfork最初是因为fork没有实现COW机制,而很多情况下fork之后会紧接着exec,而exec的执行相当于之前fork复制的空间全部变成了无用功,所以设计了vfork。而现在fork使用了COW机制,唯一的代价仅仅是复制父进程页表的代价,所以vfork不应该出现在新的代码之中。

3.clone

clone是Linux为创建线程设计的(虽然也可以用clone创建进程)。所以可以说clone是fork的升级版本,不仅可以创建进程或者线程,还可以指定创建新的命名空间(namespace)、有选择的继承父进程的内存、甚至可以将创建出来的进程变成父进程的兄弟进程等等。

clone函数功能强大,带了众多参数,它提供了一个非常灵活自由的常见进程的方法。因此由他创建的进程要比前面2种方法要复杂。clone可以让你有选择性的继承父进程的资源,你可以选择想vfork一样和父进程共享一个虚存空间,从而使创造的是线程,你也可以不和父进程共享,你甚至可以选择创造出来的进程和父进程不再是父子关系,而是兄弟关系。

问题:clone和fork的区别:

(1) clone和fork的调用方式很不相同,clone调用需要传入一个函数,该函数在子进程中执行。

(2)clone和fork最大不同在于clone不再复制父进程的栈空间,而是自己创建一个新的。 (void *child_stack,)也就是第二个参数,需要分配栈指针的空间大小,所以它不再是继承或者复制,而是全新的创造。

 

fork底层实现

linux通过clone()系统调用实现fork()。

fork(),vfork(),和_clone()库函数都根据各自需要的参数标志去调用clone(),然后由clone()去调用do_fork()

do_fork()完成了创建中的大部分工作,它定义在kernel/fork.c中。该函数调用copy_process(),copy_process()实现的工作如下

  • 1.调用dup_task_struct()为新进程创建一个内核栈,thread_info结构和task_struct,这些值与当前进程的值相同。此时,子进程和父进程的描述符是完全相同的。
  • 2.检查新创建的这个子进程后,当前用户所拥有的进程数目没有超出给他分配的资源的限制。
  • 3.现在,子进程着手使自己与父进程区别开来。进程描述符内的许多成员都要被清0或设为初始值。进程描述符的成员值并不是继承而来的,而主要是统计信息,进程描述符中大多数的数据都是共享的。
  • 4.接下来,子进程的状态被设置为TASK_UNINTERRUPTIBLE(不可中断)以保证它不会投入运行。
  • 5.copy_process()调用copy_flags()以更新task_struct的flags成员。表明进程是否拥有超级用户权限的PF_SUPERPRIV标志被清0.表名进程还没有调用exec()函数的PF_FORKNOEXEC标志被设置。
  • 6.调用get_pid()为新进程获取一个有效的PID
  • 7.根据传递给clone()的参数标志,copy_process()拷贝或共享打开的文件,文件系统信息,信号处理函数,进程地址空间和命名空间等。在一般情况下,这些资源会被给定进程的所有线程共享;否则,这些资源对每个进程是不同的,因此被拷贝到这里。
  • 8.让父进程和子进程平分剩余的时间片
  • 9.最后copy_process()做扫尾工作并返回一个指向子进程的指针

再回到do_fork()函数,如果copy_process函数成功返回,新创建的子进程被唤醒并让其投入运行。内核有意选择子进程首先执行。因为一般子进程都会马上调用exec()函数,这样可以避免写时拷贝的额外开销,如果父进程首先执行的话,有可能会开始向地址空间写入。

 

 

1.孤儿进程

  如果父进程先退出,子进程还没退出那么子进程将被 托孤给init进程,这是子进程的父进程就是init进程(系统1号进程),并由init进程对它们完成状态收集工作。

2.僵尸进程:一个进程使用fork创建子进程,如果子进程退出,而父进程并没有调用wait或waitpid获取(或回收)子进程的状态信息,那么子进程的进程描述符PCB仍然保存在系统中(为了父进程给他报仇收尸)。这种进程称之为僵死进程。

 

wait:

编程过程中,有时需要让一个进程等待另一个进程,最常见的是父进程等待自己的子进程,或者父进程回收自己的子进程资源包括僵尸进程。这里简单介绍一下系统调用函数:wait()

函数原型是

#include <sys/types.h>

#include <wait.h>

int wait(int *status)

函数功能是:父进程一旦调用了wait就立即阻塞自己,由wait自动分析是否当前进程的某个子进程已经退出,如果让它找到了这样一个已经变成僵尸的子进程,wait就会收集这个子进程的信息,并把它彻底销毁后返回;如果没有找到这样一个子进程,wait就会一直阻塞在这里,直到有一个出现为止。

注:

  当父进程忘了用wait()函数等待已终止的子进程时,子进程就会进入一种无父进程的状态,此时子进程就是僵尸进程.

  wait()要与fork()配套出现,如果在使用fork()之前调用wait(),wait()的返回值则为-1,正常情况下wait()的返回值为子进程的PID.

  如果先终止父进程,子进程将继续正常进行,只是它将由init进程(PID 1)继承,当子进程终止时,init进程捕获这个状态.

  参数status用来保存被收集进程退出时的一些状态,它是一个指向int类型的指针。但如果我们对这个子进程是如何死掉毫不在意,只想把这个僵尸进程消灭掉,(事实上绝大多数情况下,我们都会这样想),我们就可以设定这个参数为NULL,就像下面这样:

pid = wait(NULL);

如果成功,wait会返回被收集的子进程的进程ID,如果调用进程没有子进程,调用就会失败,此时wait返回-1,同时errno被置为ECHILD。

  如果参数status的值不是NULL,wait就会把子进程退出时的状态取出并存入其中, 这是一个整数值(int),指出了子进程是正常退出还是被非正常结束的,以及正常结束时的返回值,或被哪一个信号结束的等信息。由于这些信息 被存放在一个整数的不同二进制位中,所以用常规的方法读取会非常麻烦,人们就设计了一套专门的宏(macro)来完成这项工作,下面我们来学习一下其中最常用的两个:

1,WIFEXITED(status) 这个宏用来指出子进程是否为正常退出的,如果是,它会返回一个非零值。

(请注意,虽然名字一样,这里的参数status并不同于wait唯一的参数–指向整数的指针status,而是那个指针所指向的整数,切记不要搞混了。)

2, WEXITSTATUS(status) 当WIFEXITED返回非零值时,我们可以用这个宏来提取子进程的返回值,如果子进程调用exit(5)退出,WEXITSTATUS(status) 就会返回5;如果子进程调用exit(7),WEXITSTATUS(status)就会返回7。请注意,如果进程不是正常退出的,也就是说, WIFEXITED返回0,这个值就毫无意义。

一次wait只回收一个子进程,因此可用waitpid

先来看看waitpid函数的定义:

#include <sys/types.h> 
#include <sys/wait.h>
pid_t waitpid(pid_t pid,int *status,int options);
如果在调用waitpid()函数时,当指定等待的子进程已经停止运行或结束了,则waitpid()会立即返回;但是如果子进程还没有停止运行或结束,则调用waitpid()函数的父进程则会被阻塞(允许用第三个参数设为非阻塞),暂停运行。

1)pid_t pid

参数pid为欲等待的子进程识别码,其具体含义如下:

参数值      说明
pid<-1    等待进程组号为pid绝对值的任何子进程。
pid=-1    等待任何子进程,此时的waitpid()函数就退化成了普通的wait()函数。
pid=0    等待进程组号与目前进程相同的任何子进程,也就是说任何和调用waitpid()函数的进程在同一个进程组的进程。
pid>0    等待进程号为pid的子进程。
2)int *status

这个参数将保存子进程的状态信息,有了这个信息父进程就可以了解子进程为什么会推出,是正常推出还是出了什么错误。如果status不是空指针,则状态信息将被写入
器指向的位置。当然,如果不关心子进程为什么推出的话,也可以传入空指针。
Linux提供了一些非常有用的宏来帮助解析这个状态信息,这些宏都定义在sys/wait.h头文件中。主要有以下几个:
宏                                  说明
WIFEXITED(status)    如果子进程正常结束,它就返回真;否则返回假。
WEXITSTATUS(status)    如果WIFEXITED(status)为真,则可以用该宏取得子进程exit()返回的结束代码。
WIFSIGNALED(status)    如果子进程因为一个未捕获的信号而终止,它就返回真;否则返回假。
WTERMSIG(status)    如果WIFSIGNALED(status)为真,则可以用该宏获得导致子进程终止的信号代码。
WIFSTOPPED(status)    如果当前子进程被暂停了,则返回真;否则返回假。
WSTOPSIG(status)    如果WIFSTOPPED(status)为真,则可以使用该宏获得导致子进程暂停的信号代码。
3)int options

参数options提供了一些另外的选项来控制waitpid()函数的行为。如果不想使用这些选项,则可以把这个参数设为0。
主要使用的有以下两个选项:
参数                    说明
WNOHANG    如果pid指定的子进程没有结束,则waitpid()函数立即返回0,而不是阻塞在这个函数上等待;如果结束了,则返回                          该子进程的进程号。
WUNTRACED    如果子进程进入暂停状态,则马上返回。


这些参数可以用“|”运算符连接起来使用。
如果waitpid()函数执行成功,则返回子进程的进程号;如果有错误发生,则返回-1,并且将失败的原因存放在errno变量中。
失败的原因主要有:没有子进程(errno设置为ECHILD),调用被某个信号中断(errno设置为EINTR)或选项参数无效(errno设置为EINVAL)
如果像这样调用waitpid函数:waitpid(-1, status, 0),这此时waitpid()函数就完全退化成了wait()函数。
 

 

 

 

 

 

 

 

exec函数族:可让子进程做其他事

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>

int main(void)
{
    pid_t pid;
    pid = fork();
    if(pid == -1){
        perror("fork error:");
    }
    else if(pid>0){
        sleep(1);
        printf("parent\n");
    else{
        execlp("ls","ls","-l","-a","-h",NULL);
        //or     也可以是自定义的函数路径
        //execl("/bin/ls","ls","-l","-a","-h",NULL);

        //或者
        //char *argv[]={"ls","ls","-l","-a","-h",NULL};
        //execv("/bin/ls",argv);
    }
    return 0;

}

 

 

用管道进行

进程间通信:

pipe

管道是由内核管理的一个内核缓冲区(默认大小为4K),相当于我们放入内存中的一个纸条。管道的一端连接一个进程的输出。这个进程会向管道中放入信息。管道的另一端连接一个进程的输入,这个进程取出被放入管道的信息。一个缓冲区不需要很大,它被设计成为环形的数据结构(循环队列),以便管道可以被循环利用。当管道中没有信息的话,从管道中读取的进程会等待,直到另一端的进程放入信息。当管道被放满信息的时候,尝试放入信息的进程会等待,直到另一端的进程取出信息。当两个进程都终结的时候,管道也自动消失。

pipe只能在有公共祖先(有血缘关系)的进程间实现管道。

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>

int main(void)
{
    int fd[2];
    pid_t pid;    //进程号变量
    int ret = pipe(fd);
    if(ret == -1){
        perror("pipe error");
        exit(1);
    }
    pid = fork();
    if(pid == -1){
        perror("fork error");
        exit(1);
    }else if(pid==0){
        close(fd[1]);      //规定子进程读数据,所以关闭fd[1]这个写
        char buf[1024];
        ret = read(fd[0], buf, sizeof(buf));
        if(ret == 0)        //子进程走到此处
            printf("-------\n");    //本次能够读完,则ret=0,否则=独到的字符数,此处实际上应该用循环;
        write(STDOUT_FILENO,buf,ret);
        }
        
        else{
            close(fd[0]);
            write(fd[1] , "hello\n",strlen("hello\n"));
        }
        return 0;

}

流管道

命名管道

由于基于fork机制,所以管道只能用于父进程和子进程之间,或者拥有相同祖先的两个子进程之间 (有亲缘关系的进程之间)。为了解决这一问题,Linux提供了FIFO方式连接进程。FIFO又叫做命名管道(named PIPE)。

实现原理

FIFO (First in, First out)为一种特殊的文件类型,它在文件系统中有对应的路径。当一个进程以读(r)的方式打开该文件,而另一个进程以写(w)的方式打开该文件,那么内核就会在这两个进程之间建立管道,所以FIFO实际上也由内核管理,不与硬盘打交道。之所以叫FIFO,是因为管道本质上是一个先进先出的队列数据结构,最早放入的数据被最先读出来,从而保证信息交流的顺序。FIFO只是借用了文件系统(file system,命名管道是一种特殊类型的文件,因为Linux中所有事物都是文件,它在文件系统中以文件名的形式存在。)来为管道命名。写模式的进程向FIFO文件中写入,而读模式的进程从FIFO文件中读出。当删除FIFO文件时,管道连接也随之消失。FIFO的好处在于我们可以通过文件的路径来识别管道,从而让没有亲缘关系的进程之间建立连接。

api与应用

#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *filename, mode_t mode);
该函数的第一个参数是一个普通的路径名,也就是创建 后FIFO的名字。第二个参数与打开普通文件的open()函数中的mode 参数相同。 如果mkfifo的第一个参数是一个已经存在的路径名时,会返回EEXIST错误,所以一般典型的调用代码首先会检查是否返回该错误,如果确实返回该错 误,那么只要调用打开FIFO的函数就可以了。一般文件的I/O函数都可以用于FIFO,如close、read、write等等。
int mknode(const char *filename, mode_t mode | S_IFIFO, (dev_t) 0 );
其中filename是被创建的文件名称,mode表示将在该文件上设置的权限位和将被创建的文件类型(在此情况下为S_IFIFO),dev是当创建设备特殊文件时使用的一个值。因此,对于先进先出文件它的值为0。
#include<sys/types.h>  
#include<sys/stat.h>  
#include<fcntl.h>  
char *FIFO = "/tmp/my_fifo";  
main()  
{  
    char buffer[80];  
    int fd;  
    unlink(FIFO);  
    mkfifo(FIFO,0666);  
    if(fork()>0){  
    char s[ ] = “hello!\n”;  
    fd = open (FIFO,O_WRONLY);  
    write(fd,s,sizeof(s));  
    close(fd);  
    }  
    else{  
    fd= open(FIFO,O_RDONLY);  
    read(fd,buffer,80);  
    printf(“%s”,buffer);  
    close(fd);  
}  
}  

共享内存:mmap函数

mmap函数主要的功能就是将文件或设备映射到调用进程的地址空间中,当使用mmap映射文件到进程后,就可以直接操作这段虚拟地址进行文件的读写等操作,不必再调用read,write等系统调用。在很大程度上提高了系统的效率和代码的简洁性。 
使用mmap函数的主要目的是: 
- 对普通文件提供内存映射I/O,可以提供无亲缘进程间的通信; 
- 提供匿名内存映射,以供亲缘进程间进行通信。 
- 对shm_open创建的POSIX共享内存区对象进程内存映射,以供无亲缘进程间进行通信。 
下面是mmap函数的接口以及说明:

#include <sys/mman.h>  
void *mmap(void *start, size_t len, int prot, int flags, int fd, off_t offset);  
               //成功返回映射到进程地址空间的起始地址,失败返回MAP_FAILED  
start:指定描述符fd应被映射到的进程地址空间内的起始地址,它通常被设置为空指针NULL,这告诉内核自动                            
     选择起始地址,该函数的返回值即为fd映射到内存区的起始地址。
len:映射到进程地址空间的字节数,它从被映射文件开头的第offset个字节处开始,offset通常被设置为0。

prot:内存映射区的保护由该参数来设定,通常由以下几个值组合而成:
     PROT_READ:数据可读;
     PROT_WRITE:数据可写;
     PROT_EXEC:数据可执行;
     PROT_NONE:数据不可访问;
flags:设置内存映射区的类型标志,POSIX标志定义了以下三个标志:
     MAP_SHARED:该标志表示,调用进程对被映射内存区的数据所做的修改对于共享该内存区的所有进程都     
     可见,而且确实改变其底层的支撑对象(一个文件对象或是一个共享内存区对象)。
     MAP_PRIVATE:调用进程对被映射内存区的数据所做的修改只对该进程可见,而不改变其底层支撑对象。
     MAP_FIXED:该标志表示准确的解释start参数,一般不建议使用该标志,对于可移植的代码,应该把    
     start参数置为NULL,且不指定MAP_FIXED标志。
    上面三个标志是在POSIX.1-2001标准中定义的,其中MAP_SHARED和MAP_PRIVATE必须选择一个。在    
     Linux中也定义了一些非标准的标志,例如MAP_ANONYMOUS(MAP_ANON),MAP_LOCKED等,具体参考        
      Linux手册。
fd:有效的文件描述符。如果设定了MAP_ANONYMOUS(MAP_ANON)标志,在Linux下面会忽略fd参数,而有的 
     系统实现如BSD需要置fd为-1;
offset:相对文件的起始偏移。

mmap成功后,可以关闭fd,一般也是这么做的,这对该内存映射没有任何影响。

mmap实现父子进程间通信:

 

结果:

映射区共享,故*p一样,而全局变量var不变,因为父子进程不共享0-3G的地址空间

 

(2)通过内存映射文件提供无亲缘进程间的通信 
通过在不同进程间对同一内存映射文件进行映射,来进行无亲缘进程间的通信,如下测试代码:

//process 1  
#include <iostream>  
#include <cstring>  
#include <errno.h>  

#include <unistd.h>  
#include <fcntl.h>  
#include <sys/mman.h>  

using namespace std;  

#define  PATH_NAME "/tmp/memmap"  

int main()  
{  
    int *memPtr;  
    int fd;  

    fd = open(PATH_NAME, O_RDWR | O_CREAT, 0666);  
    if (fd < 0)  
    {  
        cout<<"open file "<<PATH_NAME<<" failed...";  
        cout<<strerror(errno)<<endl;  
        return -1;  
    }  

    ftruncate(fd, sizeof(int));  

    memPtr = (int *)mmap(NULL, sizeof(int), PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);  
    close(fd);  

    if (memPtr == MAP_FAILED)  
    {  
        cout<<"mmap failed..."<<strerror(errno)<<endl;  
        return -1;  
    }  

    *memPtr = 111;  
    cout<<"process:"<<getpid()<<" send:"<<*memPtr<<endl;  

    return 0;  
}  

//process 2  
#include <iostream>  
#include <cstring>  
#include <errno.h>  

#include <unistd.h>  
#include <fcntl.h>  
#include <sys/mman.h>  

using namespace std;  

#define  PATH_NAME "/tmp/memmap"  

int main()  
{  
    int *memPtr;  
    int fd;  

    fd = open(PATH_NAME, O_RDWR | O_CREAT, 0666);  
    if (fd < 0)  
    {  
        cout<<"open file "<<PATH_NAME<<" failed...";  
        cout<<strerror(errno)<<endl;  
        return -1;  
    }  

    memPtr = (int *)mmap(NULL, sizeof(int), PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);  
    close(fd);  

    if (memPtr == MAP_FAILED)  
    {  
        cout<<"mmap failed..."<<strerror(errno)<<endl;  
        return -1;  
    }  

    cout<<"process:"<<getpid()<<" receive:"<<*memPtr<<endl;  

    return 0;  
}  
执行结果如下:
# ./send   
process:12711 send:111  
# ./recv   
process:12712 receive:111  

上面的代码都没进行同步操作,在实际的使用过程要考虑到进程间的同步,通常会用信号量来进行共享内存的同步。

信号:

一般说来,信号产生后就会马上递达并产生作用,但是若在信号屏蔽字中设为1,则此信号会在未决信号集中被置1,,知道后来递达才会置0.

1.信号本质 

(kill -l 可查看所有信号 

软中断信号(signal,又简称为信号)用来通知进程发生了异步事件。在软件层次上是对中断机制的一种模拟,在原理上,一个进程收到一个信号与处理器收到一个中断请求可以说是一样的。信号是进程间通信机制中唯一的异步通信机制,一个进程不必通过任何操作来等待信号的到达,事实上,进程也不知道信号到底什么时候到达。进程之间可以互相通过系统调用kill发送软中断信号。内核也可以因为内部事件而给进程发送信号,通知进程发生了某个事件。信号机制除了基本通知功能外,还可以传递附加信息。

 

收到信号的进程对各种信号有不同的处理方法。处理方法可以分为三类:

第一种是类似中断的处理程序,对于需要处理的信号,进程可以指定处理函数,由该函数来处理。

第二种方法是,忽略某个信号,对该信号不做任何处理,就象未发生过一样。

第三种方法是,对该信号的处理保留系统的默认值,这种缺省操作,对大部分的信号的缺省操作是使得进程终止。进程通过系统调用signal来指定进程对某个信号的处理行为。

2       信号的种类

可以从两个不同的分类角度对信号进行分类:

可靠性方面:可靠信号与不可靠信号;

与时间的关系上:实时信号与非实时信号。

 

2.1    可靠信号与不可靠信号

Linux信号机制基本上是从Unix系统中继承过来的。早期Unix系统中的信号机制比较简单和原始,信号值小于SIGRTMIN的信号都是不可靠信号。这就是"不可靠信号"的来源。它的主要问题是信号可能丢失。

 

随着时间的发展,实践证明了有必要对信号的原始机制加以改进和扩充。由于原来定义的信号已有许多应用,不好再做改动,最终只好又新增加了一些信号,并在一开始就把它们定义为可靠信号,这些信号支持排队,不会丢失。

 

信号值位于SIGRTMIN和SIGRTMAX之间的信号都是可靠信号,可靠信号克服了信号可能丢失的问题。Linux在支持新版本的信号安装函数sigation()以及信号发送函数sigqueue()的同时,仍然支持早期的signal()信号安装函数,支持信号发送函数kill()。

 

信号的可靠与不可靠只与信号值有关,与信号的发送及安装函数无关。目前linux中的signal()是通过sigation()函数实现的,因此,即使通过signal()安装的信号,在信号处理函数的结尾也不必再调用一次信号安装函数。同时,由signal()安装的实时信号支持排队,同样不会丢失。

 

对于目前linux的两个信号安装函数:signal()及sigaction()来说,它们都不能把SIGRTMIN以前的信号变成可靠信号(都不支持排队,仍有可能丢失,仍然是不可靠信号),而且对SIGRTMIN以后的信号都支持排队。这两个函数的最大区别在于,经过sigaction安装的信号都能传递信息给信号处理函数,而经过signal安装的信号不能向信号处理函数传递信息。对于信号发送函数来说也是一样的。

 

2.2    实时信号与非实时信号

早期Unix系统只定义了32种信号,前32种信号已经有了预定义值,每个信号有了确定的用途及含义,并且每种信号都有各自的缺省动作。如按键盘的CTRL ^C时,会产生SIGINT信号,对该信号的默认反应就是进程终止。后32个信号表示实时信号,等同于前面阐述的可靠信号。这保证了发送的多个实时信号都被接收。

 

非实时信号都不支持排队,都是不可靠信号;实时信号都支持排队,都是可靠信号。

      信号的发送

发送信号的主要函数有:kill()、raise()、 sigqueue()、alarm()、setitimer()以及abort()。

 

kill()

#include <sys/types.h>

#include <signal.h>

int kill(pid_t pid,int signo)

 

该系统调用可以用来向任何进程或进程组发送任何信号。参数pid的值为信号的接收进程

pid>0 进程ID为pid的进程

pid=0 同一个进程组的进程

pid<0 pid!=-1 进程组ID为 -pid的所有进程

pid=-1 除发送进程自身外,所有进程ID大于1的进程

 

Sinno是信号值,当为0时(即空信号),实际不发送任何信号,但照常进行错误检查,因此,可用于检查目标进程是否存在,以及当前进程是否具有向目标发送信号的权限(root权限的进程可以向任何进程发送信号,非root权限的进程只能向属于同一个session或者同一个用户的进程发送信号)。

 

Kill()最常用于pid>0时的信号发送。该调用执行成功时,返回值为0;错误时,返回-1,并设置相应的错误代码errno。下面是一些可能返回的错误代码:

EINVAL:指定的信号sig无效。

ESRCH:参数pid指定的进程或进程组不存在。注意,在进程表项中存在的进程,可能是一个还没有被wait收回,但已经终止执行的僵死进程。

EPERM: 进程没有权力将这个信号发送到指定接收信号的进程。因为,一个进程被允许将信号发送到进程pid时,必须拥有root权力,或者是发出调用的进程的UID 或EUID与指定接收的进程的UID或保存用户ID(savedset-user-ID)相同。如果参数pid小于-1,即该信号发送给一个组,则该错误表示组中有成员进程不能接收该信号。

 

  sigqueue()

#include <sys/types.h>

#include <signal.h>

int sigqueue(pid_t pid, int sig, const union sigval val)

调用成功返回 0;否则,返回 -1。

 

sigqueue()是比较新的发送信号系统调用,主要是针对实时信号提出的(当然也支持前32种),支持信号带有参数,与函数sigaction()配合使用。

sigqueue的第一个参数是指定接收信号的进程ID,第二个参数确定即将发送的信号,第三个参数是一个联合数据结构union sigval,指定了信号传递的参数,即通常所说的4字节值。

typedef union sigval {

               int  sival_int;

               void *sival_ptr;

}sigval_t;

 

sigqueue()比kill()传递了更多的附加信息,但sigqueue()只能向一个进程发送信号,而不能发送信号给一个进程组。如果signo=0,将会执行错误检查,但实际上不发送任何信号,0值信号可用于检查pid的有效性以及当前进程是否有权限向目标进程发送信号。

 

在调用sigqueue时,sigval_t指定的信息会拷贝到对应sig 注册的3参数信号处理函数的siginfo_t结构中,这样信号处理函数就可以处理这些信息了。由于sigqueue系统调用支持发送带参数信号,所以比kill()系统调用的功能要灵活和强大得多。

 

  alarm()

#include <unistd.h>

unsigned int alarm(unsigned int seconds)

如在程序中

alarm(1);   --一秒后发送信号,终止本程序

系统调用alarm安排内核为调用进程在指定的seconds秒后发出一个SIGALRM的信号,则此程序终止。如果指定的参数seconds为0,则不再发送 SIGALRM信号。后一次设定将取消前一次的设定(因为每个进程只有一个闹钟),该调用返回值为上次定时调用到发送之间剩余的时间,或者因为没有前一次定时调用而返回0。取消定时器用alarm(0).

 

注意,在使用时,alarm只设定为发送一次信号,如果要多次发送,就要多次使用alarm调用。

 

  setitimer()

现在的系统中很多程序不再使用alarm调用,而是使用setitimer调用来设置定时器,用getitimer来得到定时器的状态,这两个调用的声明格式如下:

int getitimer(int which, struct itimerval *value);

int setitimer(int which, const struct itimerval *value, struct itimerval *ovalue);

在使用这两个调用的进程中加入以下头文件:

#include <sys/time.h>

 

该系统调用给进程提供了三个定时器,它们各自有其独有的计时域,当其中任何一个到达,就发送一个相应的信号给进程,并使得计时器重新开始。三个计时器由参数which指定,如下所示:

TIMER_REAL:按实际时间计时,计时到达将给进程发送SIGALRM信号。

ITIMER_VIRTUAL:仅当进程执行时才进行计时。计时到达将发送SIGVTALRM信号给进程。

ITIMER_PROF:当进程执行时和系统为该进程执行动作时都计时。与ITIMER_VIR-TUAL是一对,该定时器经常用来统计进程在用户态和内核态花费的时间。计时到达将发送SIGPROF信号给进程。

 

定时器中的参数value用来指明定时器的时间,其结构如下:

struct itimerval {

        struct timeval it_interval; /* 下一次的取值 */

        struct timeval it_value; /* 本次的设定值 */

};

 

该结构中timeval结构定义如下:

struct timeval {

        long tv_sec; /* 秒 */

        long tv_usec; /* 微秒,1秒 = 1000000 微秒*/

};

 

在setitimer 调用中,参数ovalue如果不为空,则其中保留的是上次调用设定的值。定时器将it_value递减到0时,产生一个信号,并将it_value的值设定为it_interval的值,然后重新开始计时,如此往复。当it_value设定为0时,计时器停止,或者当它计时到期,而it_interval 为0时停止。调用成功时,返回0;错误时,返回-1,并设置相应的错误代码errno:

EFAULT:参数value或ovalue是无效的指针。

EINVAL:参数which不是ITIMER_REAL、ITIMER_VIRT或ITIMER_PROF中的一个。

下面是关于setitimer调用的一个简单示范,在该例子中,每隔一秒发出一个SIGALRM,每隔0.5秒发出一个SIGVTALRM信号:

 

#include <signal.h>

#include <unistd.h>

#include <stdio.h>

#include <sys/time.h>

int sec;

 

void sigroutine(int signo) {

        switch (signo) {

        case SIGALRM:

        printf("Catch a signal -- SIGALRM ");

        break;

        case SIGVTALRM:

        printf("Catch a signal -- SIGVTALRM ");

        break;

        }

        return;

}

 

int main()

{

        struct itimerval value,ovalue,value2;

        sec = 5;

 

        printf("process id is %d ",getpid());

        signal(SIGALRM, sigroutine);

        signal(SIGVTALRM, sigroutine);

 

        value.it_value.tv_sec = 5;    //第一次定时5s

        value.it_value.tv_usec = 0;

        value.it_interval.tv_sec = 3;  //接下来间隔3s发出一个时间信号

        value.it_interval.tv_usec = 0;

        setitimer(ITIMER_REAL, &value, &ovalue);

 

        value2.it_value.tv_sec = 0;

        value2.it_value.tv_usec = 500000;

        value2.it_interval.tv_sec = 0;

        value2.it_interval.tv_usec = 500000;

        setitimer(ITIMER_VIRTUAL, &value2, &ovalue);

 

        for (;;) ;

}

 

该例子的屏幕拷贝如下:

localhost:~$ ./timer_test

process id is 579

Catch a signal – SIGVTALRM

Catch a signal – SIGALRM

Catch a signal – SIGVTALRM

Catch a signal – SIGVTALRM

Catch a signal – SIGALRM

Catch a signal –GVTALRM

 

 abort()

#include <stdlib.h>

void abort(void);

向进程发送SIGABORT信号,默认情况下进程会异常退出,当然可定义自己的信号处理函数。即使SIGABORT被进程设置为阻塞信号,调用abort()后,SIGABORT仍然能被进程接收。该函数无返回值。

 

 raise()

#include <signal.h>

int raise(int signo)

向进程本身发送信号,参数为即将发送的信号值。调用成功返回 0;否则,返回 -1。

 

信号的捕捉

 

 signal()

#include <signal.h>

void (*signal(int signum, void (*handler))(int)))(int);

如果该函数原型不容易理解的话,可以参考下面的分解方式来理解:

typedef void (*sighandler_t)(int);

sighandler_t signal(int signum, sighandler_t handler));

第一个参数指定信号的值,第二个参数指定针对前面信号值的处理,可以忽略该信号(参数设为SIG_IGN);可以采用系统默认方式处理信号(参数设为SIG_DFL);也可以自己实现处理方式(参数指定一个函数地址)。

如果signal()调用成功,返回最后一次为安装信号signum而调用signal()时的handler值;失败则返回SIG_ERR。

传递给信号处理例程的整数参数是信号值,这样可以使得一个信号处理例程处理多个信号。

例程:其中myfunc是自己实现的函数,告诉系统内核捕捉到信号后做什么(而不是做默认动作如终止程序),signal函数实现对SIGALRM信号的捕捉函数的注册

sigaction()

#include <signal.h>

int sigaction(int signum,const struct sigaction *act,struct sigaction *oldact));

sigaction函数用于改变进程接收到特定信号后的行为。该函数的第一个参数为信号的值,可以为除SIGKILL及SIGSTOP外的任何一个特定有效的信号(为这两个信号定义自己的处理函数,将导致信号安装错误)。第二个参数是指向结构sigaction的一个实例的指针,在结构sigaction的实例中,指定了对特定信号的处理,可以为空,进程会以缺省方式对信号处理;第三个参数oldact指向的对象用来保存返回的原来对相应信号的处理,可指定oldact为NULL。如果把第二、第三个参数都设为NULL,那么该函数可用于检查信号的有效性。

 

第二个参数最为重要,其中包含了对指定信号的处理、信号所传递的信息、信号处理函数执行过程中应屏蔽掉哪些信号等等。

sigaction结构定义如下:

struct sigaction {

                       union{

                               __sighandler_t _sa_handler;

                               void (*_sa_sigaction)(int,struct siginfo *, void *);

                       }_u

            sigset_t sa_mask;

            unsigned long sa_flags;

}

 

1、联合数据结构中的两个元素_sa_handler以及*_sa_sigaction指定信号关联函数,即用户指定的信号处理函数。除了可以是用户自定义的处理函数外,还可以为SIG_DFL(采用缺省的处理方式),也可以为SIG_IGN(忽略信号)。

 

2、由_sa_sigaction是指定的信号处理函数带有三个参数,是为实时信号而设的(当然同样支持非实时信号),它指定一个3参数信号处理函数。第一个参数为信号值,第三个参数没有使用,第二个参数是指向siginfo_t结构的指针,结构中包含信号携带的数据值,参数所指向的结构如下:

siginfo_t {

                  int      si_signo;  /* 信号值,对所有信号有意义*/

                  int      si_errno;  /* errno值,对所有信号有意义*/

                  int      si_code;   /* 信号产生的原因,对所有信号有意义*/

                               union{                               /* 联合数据结构,不同成员适应不同信号 */

                                       //确保分配足够大的存储空间

                                       int _pad[SI_PAD_SIZE];

                                       //对SIGKILL有意义的结构

                                       struct{

                                                      ...

                                                 }...

                                               ... ...

                                               ... ...                               

                                       //对SIGILL, SIGFPE, SIGSEGV, SIGBUS有意义的结构

                                  struct{

                                                      ...

                                                 }...

                                               ... ...

                                         }

}

 

前面在讨论系统调用sigqueue发送信号时,sigqueue的第三个参数就是sigval联合数据结构,当调用sigqueue时,该数据结构中的数据就将拷贝到信号处理函数的第二个参数中。这样,在发送信号同时,就可以让信号传递一些附加信息。信号可以传递信息对程序开发是非常有意义的。

 

3、sa_mask指定在信号处理程序执行过程中,哪些信号应当被阻塞。缺省情况下当前信号本身被阻塞,防止信号的嵌套发送,除非指定SA_NODEFER或者SA_NOMASK标志位。

注:请注意sa_mask指定的信号阻塞的前提条件,是在由sigaction()安装信号的处理函数执行过程中由sa_mask指定的信号才被阻塞。

 

4、sa_flags中包含了许多标志位,包括刚刚提到的SA_NODEFER及SA_NOMASK标志位。另一个比较重要的标志位是SA_SIGINFO,当设定了该标志位时,表示信号附带的参数可以被传递到信号处理函数中,因此,应该为sigaction结构中的sa_sigaction指定处理函数,而不应该为sa_handler指定信号处理函数,否则,设置该标志变得毫无意义。即使为sa_sigaction指定了信号处理函数,如果不设置SA_SIGINFO,信号处理函数同样不能得到信号传递过来的数据,在信号处理函数中对这些信息的访问都将导致段错误(Segmentation fault)。

 信号集及信号集操作函数:

信号集被定义为一种数据类型

typedef struct {

                       unsigned long sig[_NSIG_WORDS];

} sigset_t

信号集用来描述信号的集合,每个信号占用一位。Linux所支持的所有信号可以全部或部分的出现在信号集中,主要与信号阻塞相关函数配合使用。下面是为信号集操作定义的相关函数:

 

#include <signal.h>

int sigemptyset(sigset_t *set);     //注意传进去的应该是set的地址

int sigfillset(sigset_t *set);

int sigaddset(sigset_t *set, int signum)   //将某信号置1 ,影响阻塞信号集后表示将此信号屏蔽,屏蔽位置1

int sigdelset(sigset_t *set, int signum);

int sigismember(const sigset_t *set, int signum);    //判断某信号是否在集合中

sigemptyset(sigset_t *set)初始化由set指定的信号集,信号集里面的所有信号被清空;

sigfillset(sigset_t *set)调用该函数后,set指向的信号集中将包含linux支持的64种信号;

sigaddset(sigset_t *set, int signum)在set指向的信号集中加入signum信号;

sigdelset(sigset_t *set, int signum)在set指向的信号集中删除signum信号;

sigismember(const sigset_t *set, int signum)判定信号signum是否在set指向的信号集中。

信号阻塞与信号未决:

每个进程都有一个用来描述哪些信号递送到进程时将被阻塞的信号集,该信号集中的所有信号在递送到进程后都将被阻塞。下面是与信号阻塞相关的几个函数:

#include <signal.h>

int  sigprocmask(int  how,  const  sigset_t *set, sigset_t *oldset));

int sigpending(sigset_t *set));

int sigsuspend(const sigset_t *mask));

 

sigprocmask()函数能够根据参数how来实现对信号集的操作,操作主要有三种:

SIG_BLOCK 在进程当前阻塞信号集中添加(自定义的)set指向信号集中的信号

SIG_UNBLOCK 如果进程阻塞信号集中包含set指向信号集中的信号,则解除对该信号的阻塞

SIG_SETMASK 更新进程阻塞信号集为set指向的信号集

 

sigpending(sigset_t *set))获得当前已递送到进程,却被阻塞的所有信号,在set指向的信号集中返回结果。

 

sigsuspend(const sigset_t *mask))用于在接收到某个信号之前, 临时用mask替换进程的信号掩码, 并暂停进程执行,直到收到信号为止。sigsuspend 返回后将恢复调用之前的信号掩码。信号处理函数完成后,进程将继续执行。该系统调用始终返回-1,并将errno设置为EINTR。

 

  • 3
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值