Linux Communication Mechanism Summarize

目录

1. Linux通信机制分类简介
2. 控制机制
    0x1: 竞态条件
    0x2: 临界区
2. Inter-Process Communication (IPC) mechanisms: 进程间通信机制
    0x1: 信号(Signals)
    0x2: 管道(Pipes) 
    0x3: 套接字(Sockets)
    0x4: System V通信机制(System V IPC Mechanisms) 
3. 多线程并行中的阻塞和同步
    0x1: CPU指令集提供的原子操作(Atomic)
    0x2: 操作系统提供的原子操作API
    0x3: 同步与锁
    0x4: 二元信号量(binary semaphore)
    0x5: 信号量(多元信号量)
    0x6: 互斥量(mutex)
    0x7: 临界区(critical section)
    0x8: 读写锁(read-write lock)
    0x9: 条件变量(condition variable)
4. Ring3和Ring0的通信机制
    0x1: Sharing Memory Between Drivers and Applications
    0x2: Sharing Events Between Kernel-User Mode
    0x3: Netlink技术: communication between kernel and user space with netlink(AF_NETLINK)
    0x4: 内核启动参数
    0x5: 模块参数、sysfs简单数据共享传输
    0x6: Debugfs
    0x7: sysctl
    0x8: ioctl简单数据共享传输
    0x9: procfs简单数据共享传输
    0x10: Character Devices
    0x11: UDP Sockets
    0x12: Sending Signals from the Kernel to the User Space
    0x13: Upcall
    0x14: mmap Portable Operating System Interface for UNIX® (POSIX) 共享的内存机制(shmem)共享内存  
    0x15: SYS V Message Queues Between KERNEL AND USER SPACE
    0x16: 内核的段描述符号突破Linux保护模式实现内核态用户态内存互写
5. 远程网络通信

 

1. Linux通信机制简介

在开始学习Linux下的通信机制之前,我们先来给通信机制下一个定义,即明白什么是通信机制?为什么要存在通信机制?

0x1: Linux通信目的

1. 数据传输: 一个进程需要将它的数据发送给另一个进程,发送的数据量在一个字节到几兆字节之间
2. 共享数据: 多个进程想要操作共享数据,一个进程对共享数据的修改,别的进程应该立刻看到
3. 通知事件: 一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程),从广义上讲,事件也是一种数据(数据量很小的数据),只不过这段数据的目的在于标识另一个事件的发生,
而不是自身的意义
4. 资源共享: 多个进程之间共享同样的资源。为了作到这一点,需要内核提供锁和同步机制
5. 进程控制: 有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变,为了实现进程互斥同步控制,进程间往往会发送一些信号量,从本质上
理解,这些信号量同样也是数据(即你也可以将它们理解为数据传输),差别在于这些数据量往往很小(甚至只有1bit),仅仅用于通知某种条件的达成与否而存在的

如果几个进程共享一个资源,则很容易彼此干扰,必须防止这种情况,因此内核不仅提供了共享数据的机制,同样提供了协调对数据访问的机制,它们都来自SystemV的机制。从本质上理解,数据共享和数据协调同步就是用小数据(甚至1bit)的共享来协调大数据(几K到几M)的共享,这里面结合了信号通知机制
用户空间应用程序和内核自身都需要保护资源,特别是内核,在SMP系统上,各个CPU可能同时处于和心态,在理论上可以操作所有现存的数据结构,为阻止CPU彼此干扰,需要通过锁保护内核的某些范围,锁可以确保每次只能有一个CPU访问被保护的范围

0x2: 通信机制的分类

Linux下的通信机制是一个大的概念,我们可以理解为任何需要和别的模块进行交互、协作的模块组件都会涉及到通信机制,就像我们生活中一样,我们需要和各种人和机构进行"通信",Linux系统中也一样,总体来说,我们在操纵系统这个层面上所谈的通信机制包含以下方面

1. 进程间通信机制
2. 多线程并行中的阻塞和同步
3. Ring3和Ring0的通信
4. 远程网络通信

值得注意的是,

1. 这4个方面是Linux下通信机制的4个不同方面,而要实现这些机制需要有对应的技术,每种机制都会有多个技术方案的支持,同样,单个技术方案也可能同时支持多种机制,我们在学习操作系统原理的时候,一定要明白机制和技术的
关系
2. 我们可能在学习编程技术的(例如C#、JAVA..)时会看到很多的进程间通信的API、类库、函数等等,我们必须明白的是编程语言所使用到的技术都是基于操作系统提供的特性实现的。也就是说,C#/JAVA中的很多延时触发、异步通
信技术的底层原理都是操作系统的通信机制,我们在学习的时候要注意理解它们之间的从属关系,不要混淆了

 

2. 控制机制

在讨论各种进程间通信(interprocess communication IPC)和数据同步机制之前,我们先来学习一下相互通信的进程彼此干扰的可能的情况,以及如何防止

0x1: 竞态条件

我们考虑通过两种接口从外部设备读取数据的情况,独立的数据包以不定间隔通过两个接口到达,保存在不同文件中,为记录数据包到达的次序,在文件名之后添加了一个号码,表明数据包的序号: act1.file、act2.file、act3.file、act4.file,可使用一个独立的变量来简化两个进程的工作,该变量保存在由两个进程共享的内存页中,且指定了下一个未使用的序号
在一个数据包到达时,进程必须执行一些操作,才能正确地保存数据

1. 从接口读取数据
2. 用序号counter构造文件名,打开一个文件
3. 将序号加1
4. 将数据写入文件,然后关闭文件

我们思考一个竞态条件发生的场景

1. 进程1从接口接收一个刚到达的新数据块,它使用一个新的序列号(例如13)构造文件名并打开一个文件,而同时调度器被激活并确认该进程已经消耗了足够的CPU时间,必须由另一个进程(进程2)替换,要注意的是,此时进程1读取了counter的值,但尚未对counter加1
2. 进程2开始运行后,同样从其对应的接口读取数据,并开始执行必要的操作以保存这些数据,它会读取counter的值,用序号13构造文件名打开文件(实际上内核是将进程1创建的文件的句柄返回,结果是进程2和进程1打开了同一个文件),将counter加1,counter从13变为14,接下来它将数据写入文件,最后结束。
3. 不久后,调度器再次让进程继续运行,它从上次暂停处恢复执行,并将counter加1,counter从14变为15,接下来它将数据写入用序号13打开的文件,这样做的时候,会覆盖进程2已经保存的数据

几个进程在访问资源时彼此干扰的情况通常称之为竞态条件(race conditions),在对分布式应用编程时,这种情况是一个主要的问题,因为竞态条件无法通过系统的"试错法"检测,相反,只有彻底研究源代码(深入了解各种可能发生的代码路径)并通过敏感的判断,才能找到并消除竞态条件
0x2: 临界区

临界区的本质是: 进程的执行在不应该的地方被中断,从而导致进程工作得不正确,显然,问题的解决方案不一定要求临界区是不能中断的,只要没有其他进程进入临界区,那么在临界区中执行的进程完全可以中断的,这种严格的禁止条件,可以确保几个进程不能同时改变共享的值,我们称为互斥(mutual exclusion),也就是说,在给定时刻,只有一个进程可以进入临界区代码

 

2. Inter-Process Communication (IPC) mechanisms: 进程间通信机制

进程间通信(Inter-Process Communication (IPC) mechanisms)中涉及到的技术主要包括

1. 信号(Signals)
2. 管道(Pipes)
    1)普通管道: PIPE
    对于普通管道,我们要注意它通常有两个限制:
        1.1) 单工,只能单向传输
        1.2) 只能在父子或者兄弟进程间使用
    2)流管道: s_pipe
        2.1) 半双工的管道,可以双向传输
        2.2) 但同样只能在父子或者兄弟进程间使用
    3)命名管道: name_pipe
        3.1) 单工,只能单向传输
        3.2) 可以在许多并不相关的进程之间进行通讯
3. 套接字(Sockets)
4. System V通信机制(System V IPC Mechanisms)
     1) 共享内存(Shared Memory)
     2) 信号量(Semaphores)
     3) 消息队列(Message Queues)

0x1: 信号(Signals)

除了System V UNIX采用的IPC机制之外,进程之间还有其他传统的方法可用于交换消息和数据,SysV IPC通常只对应用程序员有意义,但是对于shell来说,信号和管道是非常方便高效的机制
信号是在软件层次上对中断机制的一种模拟,在原理上,一个进程收到一个信号与处理器收到一个中断请求可以说是一样的。信号是异步的,一个进程不必通过任何操作来等待信号的到达,事实上,进程也不知道信号到底什么时候到达。值得注意的是:

信号是进程间通信机制中"唯一""异步通信机制",可以看作是异步通知,通知接收信号的进程有哪些事情发生了

信号事件的发生有两个来源

1. 硬件来源
    1) 比如我们按下了键盘
    2) 其它硬件故障
2. 软件来源
最常用发送信号的系统函数是
    1) kill
    2) raise
    3) alarm
    4) setitimer
    5) sigqueue函数
    6) 非法运算等操作

在老的Linux内核中,32位系统最多支持32个信号,该限制在现在已经提高了,经典的信号占用了信号列表中的前32个位置,接下来是针对实时进程引入的新信号
Linux下存在的信号有:

kill -l
1. SIGHUP: Hangup
2. SIGINT: Interrupt
3. SIGQUIT: Quit and dump core
4. SIGILL: Illegal instruction
5. SIGTRAP: Trace/breakpoint trap
6. SIGABRT: Process aborted
7. SIGBUS: Bus error: "access to undefined portion of memory object"
8. SIGFPE: Floating point exception: "erroneous arithmetic operation"
9. SIGKILL: Kill (terminate immediately)
10. SIGUSR1: User-defined 1
11. SIGSEGV: Segmentation violation
12. SIGUSR2: User-defined 2
13. SIGPIPE: Write to pipe with no one reading
14. SIGALRM: Signal raised by alarm
15. SIGTERM: Termination (request to terminate)
16. SIGSTKFLT    
17. SIGCHLD: Child process terminated, stopped (or continued*)
18. SIGCONT: Continue if stopped
19. SIGSTOP: Stop executing temporarily
20. SIGTSTP: Terminal stop signal
21. SIGTTIN: Background process attempting to read from tty ("in")
22. SIGTTOU: Background process attempting to write to tty ("out")
23. SIGURG: Urgent data available on socket
24. SIGXCPU: CPU time limit exceeded
25. SIGXFSZ: File size limit exceeded
26. SIGVTALRM: Signal raised by timer counting virtual time: "virtual timer expired"
27. SIGPROF: Profiling timer expired
28. SIGWINCH    
29. SIGIO: Pollable event
30. SIGPWR
31. SIGSYS: Bad syscall
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

进程必须设置处理程序例程来处理信号,这些例程在信号发送到进程时调用(但有几个信号的行为无法修改,例如SIGKILL),如果没有显示设置处理程序例程,内核则使用默认的处理程序实现
信号引入了几种特性

1. 进程可以阻塞特定的信号(即信号屏蔽),如果发生这种情况,进程会一直忽略该信号,直至进程决定解除阻塞,因而,进程是否能感知到发送的信号,操作系统是不能保证的。在信号被阻塞时,内核将其放置到待决列表上,如果同一个信号被阻塞多次,则在待决列表中只放置一次,不管发送多少相同的信号,在进程删除阻塞之后,都只会接收到一个信号
2. SIGKILL信号无法阻塞,也不能通过特定于进程的处理程序处理,这是因为它是从系统删除失控进程的最后手段,内核需要立即强行终止程序
3. init进程属于特例,内核会忽略发送给该进程的SIGKILL信号,即不能强制结束该进程

我们在编程中,如果需要使用异步通信机制的信号技术,就需要借助相应的API来得以实现

1. 信号的安装(设置信号关联动作)

如果进程要处理某一信号,那么就要在进程中安装该信号。安装信号的目的主要有两个

1. 确定信号值: 进程将要处理哪个信号
2. 确定进程针对该信号值的动作之间的映射关系: 该信号被传递给进程时,将执行何种操作

linux主要有两个函数实现信号的安装:

1. signal() 
#include <signal.h> 
void (*signal(int signum, void (*handler))(int)))(int); 
    1) signum: 指定信号的值
    2) *handler: 指定针对前面信号值的处理
        2.1) 忽略该信号(参数设为SIG_IGN)
        2.2) 采用系统默认方式处理信号(参数设为SIG_DFL)
        2.3) 自己实现处理方式(参数指定一个函数地址) 
如果signal()调用成功,返回最后一次为安装信号signum而调用signal()时的handler值;失败则返回SIG_ERR

2. sigaction() 
#include <signal.h> 
int sigaction(int signum, const struct sigaction *act,struct sigaction *oldact));
sigaction函数用于改变进程接收到特定信号后的行为
    1) signum: 
    信号的值,可以为除SIGKILL及SIGSTOP外的任何一个特定有效的信号(为这两个信号定义自己的处理函数,将导致信号安装错误)
    2) *act
    向结构sigaction的一个实例的指针,在结构sigaction的实例中,指定了对特定信号的处理,可以为空,进程会以缺省方式对信号处理
    3) *oldact
    指向的对象用来保存原来对相应信号的处理,可指定oldact为NULL。如果把第二、第三个参数都设为NULL,那么该函数可用于检查信号的有效性
在实际的编程中,sigaction()比传统的signal()能发挥更大的作用,对于sigaction()来说,第二个参数*act最为重要,其中包含了对指定信号的处理、信号所传递的信息、信号处理函数执行过程中应屏蔽掉哪些函数等等
sigaction结构定义如下:
struct sigaction 
{    
    /*
    指定信号关联函数
    除了可以是用户自定义的处理函数外,还可以为SIG_DFL(采用缺省的处理方式),也可以为SIG_IGN(忽略信号) 
    */
    union
    {
        /*
        由_sa_handler指定的处理函数只有一个参数,即信号值,所以信号不能传递除信号值之外的任何信息
        */
        __sighandler_t _sa_handler;
        /*
        由_sa_sigaction是指定的信号处理函数带有三个参数,是为实时信号而设的(当然同样支持非实时信号),它指定一个3参数信号处理函数
            1) 信号值
            2) 指向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
                    {
                        ...
                    }
                    ... 
                }
            } 
            3) 第三个参数没有使用(posix没有规范使用该参数的标准)
        */
        void (*_sa_sigaction)(int,struct siginfo *, void *);
    }_u

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

    /*
    sa_flags
    包含了许多标志位,包括
        1) A_NODEFER
        2) SA_NOMASK
        3) SA_SIGINFO: 
        当设定了该标志位时,表示信号附带的参数可以被传递到信号处理函数中,因此,应该为sigaction结构中的sa_sigaction指定处理函数,而不应该为sa_handler指定信号处理函数,否则,设置该标志变得毫无意义。即
使为sa_sigaction指定了信号处理函数,如果不设置SA_SIGINFO,信号处理函数同样不能得到信号传递过来的数据,在信号处理函数中对这些信息的访问都将导致段错误(Segmentation fault)
*/ unsigned long sa_flags; void (*sa_restorer)(void); //已过时,POSIX不支持它,不应再被使用 }

2. 信号的发送(触发信号机制)

发送信号的主要函数有:

1. kill() 
#include <sys/types.h> 
#include <signal.h> 
int kill(pid_t pid,int signo) 
    1) pid: 信号的接收进程
        1.1) pid>0: 进程ID为pid的进程
        1.2) pid=0: 同一个进程组的进程
        1.3) pid<0 pid!=-1: 进程组ID为"-pid"的所有进程
        1.4) pid=-1: 除发送进程自身外,所有进程ID大于1的进程
    2) Sinno: 信号值
        2.1) 0: 即空信号
        实际不发送任何信号,但照常进行错误检查,因此,可用于检查目标进程是否存在,以及当前进程是否具有向目标发送信号的2.2) 非0: 发送"kill -l"中列出的信号量
权限(root权限的进程可以向任何进程发送信号,非root权限的进程只能向属于同一个session或者同一个用户的进程发送信号)

2. raise()
#include <signal.h> 
int raise(int signo) 
向进程本身发送信号,参数为即将发送的信号值。调用成功返回0、否则,返回 -1

3. sigqueue()
#include <sys/types.h> 
#include <signal.h> 
int sigqueue(pid_t pid, int sig, const union sigval val) 
sigqueue()是比较新的发送信号系统调用,主要是针对实时信号提出的(当然也支持前32种),支持信号带有参数,与函数sigaction()配合使用。
    1) pid
    指定接收信号的进程ID
    2) sig
    确定即将发送的信号
    3) val
    是一个联合数据结构union sigval,指定了信号传递的参数,即通常所说的4字节值
     typedef union sigval 
    {
         int  sival_int;
         void *sival_ptr;
     }sigval_t;
sigqueue()比kill()传递了更多的附加信息,但sigqueue()只能向一个进程发送信号,而不能发送信号给一个进程组。如果signo=0,将会执行错误检查,但实际上不发送任何信号,0值信号可用于检查pid的有效性以及当前进程是
否有权限向目标进程发送信号
4. alarm() #include <unistd.h> unsigned int alarm(unsigned int seconds) 专门为SIGALRM信号而设,在指定的时间seconds秒后,将向进程本身发送SIGALRM信号,又称为闹钟时间 1) 进程调用alarm后,任何以前的alarm()调用都将无效 2) 如果参数seconds为零,那么进程内将不再包含任何闹钟时间 返回值,如果调用alarm()前,进程中已经设置了闹钟时间,则返回上一个闹钟时间的剩余时间,否则返回0 5. setitimer() #include <sys/time.h> int setitimer(int which, const struct itimerval *value, struct itimerval *ovalue)); 1) which setitimer()比alarm功能强大,支持3种类型的定时器: 1.1) ITIMER_REAL: 设定绝对时间;经过指定的时间后,内核将发送SIGALRM信号给本进程 1.2) ITIMER_VIRTUAL: 设定程序执行时间;经过指定的时间后,内核将发送SIGVTALRM信号给本进程 1.3) ITIMER_PROF: 设定进程执行以及内核因本进程而消耗的时间和,经过指定的时间后,内核将发送ITIMER_VIRTUAL信号给本进程 2) *value 结构itimerval的一个实例 3) *ovalue 可不做处理 6. abort() #include <stdlib.h> void abort(void); 向进程发送SIGABORT信号,默认情况下进程会异常退出,当然可定义自己的信号处理函数。即使SIGABORT被进程设置为阻塞信号,调用abort()后,SIGABORT仍然能被进程接收。该函数无返回值

3. 信号集及信号集操作API

信号集被定义为一种数据类型:
typedef struct 
{
    unsigned long sig[_NSIG_WORDS];
} sigset_t
信号集用来描述信号的集合,linux所支持的所有信号可以全部或部分的出现在信号集中,信号集需要和信号阻塞相关函数配合使用。下面是为信号集操作定义的相关函数:
#include <signal.h>
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset(sigset_t *set, int signum);
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指向的信号集中

系统调用不会触发信号队列的处理,在每次由和心态切换到用户状态时,内核都会发起信号队列处理,由于处理是在entry.S的汇编语言代码中发起的,因此实现自然非常特定于体系结构,执行该操作最终的效果就是调用do_signal函数,尽管它也是平台相关的,但在所有系统上的行为都大致相同

1. get_signal_to_deliver: 收集了与需要传送的下一个信号有关的所有信息,它也从特定于进程的待决信号链表中删除该信号
2. handle_signal: 操作进程在用户状态下的栈,使得在从和心态切换到用户状态之后运行信号处理程序,而不是正常的程序代码,这种复杂的方法是必要的,因为处理程序函数不能在核心态执行
3. 栈还会被修改,使得在处理程序函数结束时调用sigreturn系统调用,完成该工作的方式依赖于具体的体系结构,但内核或者将执行系统调用的机器代码指令直接写到栈上,或者借助用户空间中可用的一些"胶水"代码,该例程负责恢复进程上下文,使得在下一次切换到用户状态时,应用程序可以继续运行

4. 信号阻塞与信号未决

我们已经知道了如何在进程中安装信号、在其他进程中发送信号、如何操作信号量,但是在信号机子好的实际运行中,还必须考虑到信号的阻塞与未决问题,即信号发送者即使发出了信号,接受者也未必能立刻接收并响应

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

#include <signal.h>
1. int sigprocmask(int  how,  const  sigset_t *set, sigset_t *oldset));
sigprocmask()函数能够根据参数how来实现对信号集的操作,操作主要有三种
    1) SIG_BLOCK: 在进程当前阻塞信号集中添加set指向信号集中的信号
    2) SIG_UNBLOCK: 如果进程阻塞信号集中包含set指向信号集中的信号,则解除对该信号的阻塞
    3) SIG_SETMASK: 更新进程阻塞信号集为set指向的信号集

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

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

5. 信号生命周期

1. 信号"诞生"
信号的诞生指的是触发信号的事件发生(如检测到硬件异常、定时器超时以及调用信号发送函数kill()或sigqueue()等)
信号在目标进程中"注册";进程的task_struct结构中有关于本进程中未决信号的数据成员:
struct sigpending pending;
struct sigpending
{    
    /*
    分别指向一个sigqueue类型的结构链(称之为"未决信号信息链")的首尾,信息链中的每个sigqueue结构刻画一个特定信号所携带的信息,并指向下一个sigqueue结构:
    struct sigqueue
    {
        struct sigqueue *next;
        siginfo_t info;
    }
    */
    struct sigqueue *head, **tail;
    //进程中所有未决信号集
    sigset_t signal;
};
 
2. 信号在进程中注册
指的就是信号值加入到进程的未决信号集中(sigpending结构的第二个成员sigset_t signal),并且信号所携带的信息被保留到未决信号信息链的某个sigqueue结构中。只要信号在进程的未决信号集中,表明进程已经知道这些信号的
存在,但还没来得及处理,或者该信号被进程阻塞 注: 当一个实时信号发送给一个进程时,不管该信号是否已经在进程中注册,都会被再注册一次,因此,信号不会丢失,因此,实时信号又叫做
"可靠信号"。这意味着同一个实时信号可以在同一个进程的未决信号信息链中占有多个sigqueue
结构(进程每收到一个实时信号,都会为它分配一个结构来登记该信号信息,并把该结构添加在未决信号链尾,即所有诞生的实时信号都会在目标进程中注册); 当一个非实时信号发送给一个进程时,如果该信号已经在进程中注册,则该信号将被丢弃,造成信号丢失。因此,非实时信号又叫做
"不可靠信号"。这意味着同一个非实时信号在进程的未决信号信息链中,至多占有一个sigqueue结构
(一个非实时信号诞生后,(1)、如果发现相同的信号已经在目标结构中注册,则不再注册,对于进程来说,相当于不知道本次信号发生,信号丢失;(2)、如果进程的未决信号中没有相同信号,则在进程中注册自己)。 3. 信号在进程中的注销 在目标进程执行过程中,会检测是否有信号等待处理(每次从系统空间返回到用户空间时都做这样的检查)。如果存在未决信号等待处理且该信号没有被进程阻塞,则在运行相应的信号处理函数前,进程会把信号在未决信号链中占有的结构
卸掉。是否将信号从进程未决信号集中删除对于实时与非实时信号是不同的。
1) 对于非实时信号来说,由于在未决信号信息链中最多只占用一个sigqueue结构,因此该结构被释放后,应该把信号在进程未决信号集中删除(信号注销完毕) 2) 对于实时信号来说,可能在未决信号信息链中占用多个sigqueue结构,因此应该针对占用sigqueue结构的数目区别对待:如果只占用一个sigqueue结构(进程只收到该信号一次),则应该把信号在进程的未决信号集中删除
(信号注销完毕)。否则,不应该在进程的未决信号集中删除该信号(信号注销完毕) 进程在执行信号相应处理函数之前,首先要把信号在进程中注销。
4. 信号生命终止 进程注销信号后,立即执行相应的信号处理函数,执行完毕后,信号的本次发送对进程的影响彻底结束。

6. 利用操作系统提供的原生系统调用进行信号机制编程

从总体框架上来说,linux下的信号的编程需要完成3件事

1. 安装信号(推荐使用sigaction())
2. 实现三参数信号处理函数,handler(int signal,struct siginfo *info, void *);
3. 发送信号,推荐使用sigqueue()

code:

#include <signal.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>

void new_op(int,siginfo_t*,void*);
int main(int argc,char**argv)
{
    struct sigaction act;    
    int sig;
    sig=atoi(argv[1]);
    
    sigemptyset(&act.sa_mask);
    act.sa_flags=SA_SIGINFO;
    act.sa_sigaction=new_op;
    
    if(sigaction(sig,&act,NULL) < 0)
    {
        printf("install sigal error\n");
    }
    
    while(1)
    {
        sleep(2);
        printf("wait for the signal\n");
    }
}
void new_op(int signum,siginfo_t *info,void *myact)
{
    printf("receive signal %d", signum);
    sleep(5);
}

Relevant Link:

http://www.tldp.org/LDP/tlk/ipc/ipc.html
http://www.ibm.com/developerworks/cn/linux/l-ipc/part2/index1.html
http://www.ibm.com/developerworks/cn/linux/l-ipc/part2/index2.html

0x2: 管道(Pipes)

进程间通信(IPC)的另一种技术是管道(pipes)技术,要完全理解linux系统下的管道机制并不容易,因为我们可能会发现,在程序编程和shell指令中,我们都可以见到管道的身影,从本质上来说,它们都是调用了linux底层的文件系统pipefs来进行实现的,我们需要先了解几个基本概念

1. pipe是单向的
2. pipe没有对应的disk image,只有inode,当创建一个pipe时,实际上是创建了一个inode和两个file object
    1) pipe属于pipefs文件系统 
    2) 两个file object分别对应于读端、和写端
3. pipefs这个特殊的文件系统在VFS的目录中是没有的,用户不可见,它是在kernel初始化时进行创建并且挂载到VFS上的

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

值得注意的是,在Linux 中,管道的实现并没有使用专门的数据结构,而是借助了文件系统的file结构和VFS的索引节点inode。通过将两个 file 结构指向同一个临时的VFS 索引节点,而这个 VFS 索引节点又指向一个物理页面而实现的

关于fiel、inode的详细数据结构,请参阅另一篇文章

http://www.cnblogs.com/LittleHann/p/3865490.html
搜索: "文件系统相关数据结构"

从上面这张图我们可以看到,有两个 file 数据结构,但它们定义文件操作例程地址是不同的,其中一个是向管道中写入数据的例程地址,而另一个是从管道中读出数据的例程地址。这样,用户程序的系统调用仍然是通常的文件操作,而内核却利用这种抽象机制实现了管道这一特殊操作

管道实现的源代码在fs/pipe.c中,在pipe.c中有很多函数,其中有两个函数比较重要,即管道读函数pipe_read()和管道写函数pipe_wrtie()

linux-2.6.32.63\fs\pipe.c

1. static ssize_t pipe_write(struct kiocb *iocb, const struct iovec *_iov, unsigned long nr_segs, loff_t ppos)
管道写函数通过将字节复制到VFS索引节点指向的物理内存而写入数据,当然,内核必须利用一定的机制同步对管道的访问,为此,内核使用了锁、等待队列和信号。
当写进程向管道中写入时,它利用标准的库函数write(),系统根据库函数传递的文件描述符,可找到该文件的file结构。file结构中指定了用来进行写操作的函数(file_operations->...)地址,于是,内核调用该函数完成写操作。
写入函数在向内存中写入数据之前,必须首先检查VFS索引节点中的信息,同时满足如下条件时,才能进行实际的内存复制工作:
1) 内存中有足够的空间可容纳所有要写入的数据 2) 内存没有被读程序锁定(pipe会阻塞的主要原因) 如果同时满足上述条件,将开始执行管道的读写草走 1) 写入函数首先锁定内存 2) 然后从写进程的地址空间中复制数据到内存。否则,写入进程就休眠在VFS索引节点的等待队列中 3) 接下来,内核将调用调度程序,而调度程序会选择其他进程运行。写入进程实际处于可中断的等待状态 4) 当内存中有足够的空间可以容纳写入数据,或内存被解锁时,读取进程会唤醒写入进程 5) 这时,写入进程将接收到信号 6) 当数据写入内存之后,内存被解锁,而所有休眠在索引节点的读取进程会被唤醒 2. static ssize_t pipe_read(struct kiocb *iocb, const struct iovec *_iov, unsigned long nr_segs, loff_t pos) 管道读函数则通过复制VFS索引节点指向的物理内存中的的字节而读出数据,当然,内核必须利用一定的机制同步对管道的访问,为此,内核使用了锁、等待队列和信号 管道的读取过程和写入过程类似。但是,进程可以在没有数据或内存被锁定时立即返回错误信息,而不是阻塞该进程,这依赖于文件或管道的打开模式。反之,进程可以休眠在索引节点的等待队列中等待写入进程写入数据。当所有的进程
完成了管道操作之后,管道的索引节点被丢弃,而共享数据页也被释放

1. Bash命令下的管道命令执行过程

ls | more

1. shell创建一个pipe,linux系统为其创建了:
    1) 1个inode: pipe在底层创建了VFS上的一个inode,这是特殊的文件系统
    2) 2个file descriptor
        2.1) 3: 负责读端的file descriptor
        2.2) 4: 负责写端的file descriptor

2. shell fork出两个子进程
    1) ls进程
    2) more进程
这两个子进程同时继承了父进程的文件描述符,也就是说,他们同样有3和4

3. 第一个子进程(ls)调用dup2(4, 1)进行句柄复制,将原本ls的输出"1: 标准输出"替换为了"4: pipe file descriptor 管道写端",简单来说就是进程ls的输出被重定向到了管道pipe的写端中

4. 子进程(shell执行fork得到的ls子进程)执行execve(),由于execve也是用的同一个文件描述符表,所以此时ls的输出实际上是pipe的写端

5. 第二个子进程(more)调用dup2(3, 0)进行句柄复制,将原本more的输入"0: 标准输入"替换为了"3: pipe file descriptor 管道读端",简单来说就似乎进程more的输入被重定向到了管道pipe的读端中

6. 子进程(shell执行fork得到的more子进程)执行execve(),由于execve也是用的同一个文件描述符表,more的输入实际上是pipe的读端
 
7. 于是,ls的输出就顺利的重定向到more的输入了

管道命令"|"将一个进程的输出用作另一个进程的输入,管道负责数据的传输。管道是用于交换数据的连接,一个进程向管道的一端供给数据,另一个在管道另一端取出数据(这个过程只能是单向的),通过管道机制,可见将一系列的进程连接起来

2. 编程中的管道执行过程

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/wait.h>
#define BUFFER 255

int main(int argc, char **argv) 
{
    char buffer[BUFFER + 1];
    int fd[2];
    if (argc != 2) 
    {
        fprintf(stderr, "Usage:%s string\n\a", argv[0]);
        exit(1);
    }
    if (pipe(fd) != 0) 
    {
        fprintf(stderr, "Pipe Error:%s\n\a", strerror(errno));
        exit(1);
    }
    //父进程向管道写入
    if (fork() == 0) 
    {
        close(fd[0]);
        printf("Child[%d] Write to pipe\n\a", getpid());
        snprintf(buffer, BUFFER, "%s", argv[1]);
        write(fd[1], buffer, strlen(buffer));
        printf("Child[%d] Quit\n\a", getpid());
        exit(0);
    } 
    //子进程从管道中读取
    else 
    {
        close(fd[1]);
        printf("Parent[%d] Read from pipe\n\a", getpid());
        memset(buffer, '\0', BUFFER + 1);
        read(fd[0], buffer, BUFFER);
        printf("Parent[%d] Read:%s\n", getpid(), buffer);
        exit(1);
    }
}

Relevant Link:

http://my.oschina.net/u/158589/blog/54705
http://my.oschina.net/u/158589/blog/69047
http://my.oschina.net/u/158589/blog/55051 http://www.cnblogs.com/biyeymyhjob/archive/2012/11/03/2751593.html http://lobert.iteye.com/blog/1707450 http://www.cnblogs.com/sinohenu/archive/2012/11/22/2783279.html http://www.linuxidc.com/Linux/2008-10/16334p6.htm http://bbs.ednchina.com/BLOG_ARTICLE_1969989.HTM

0x3: 套接字(Sockets)

管道和套接字是常见的进程间通信机制,二者都大量使用了内核的其他子系统,管道使用饿了虚拟文件系统,而套接字使用了各种网络函数以及虚拟文件系统
套接字对象在内核中初始化时返回一个文件描述符(file dicriptor),因此可以像普通文件一样处理套接字(这就是抽象和封装机制带来的好处)(unix的哲学思想: 一切皆文件),和管道不同的是,,套接字的使用范围更广

1. 套接字可以双向使用
2. 套接字还可以用于于通过网络连接的远程系统通信
3. 套接字可以支持本地系统上两个进程之间的通信(mysql就支持socket套接字连接)

也正是因为这些方便的特性,使得套接字的实现成为内核中相当复杂的一部分,因为需要大量抽象机制来隐藏通信的细节,从用户的角度来看,同一个系统上两个本地进程之间的通信、和分别处于两个不同位置的两台计算机上运行的应用程序之间的通信没有太大的区别,就目前而言,几乎所有的应用程序都是采用socket,包括openssl的实现、mysql的通信、主流浏览器的实现都是基于socket的,所以说socket在系统通信领域的应用十分广泛(一切皆socket)

接下来,我们将从tcp socket通信、dp socket通信、本机进程间socket通信这3个方面来学习一下socket的用法、以及相关数据结构,这里不涉及socket的系统调用原理,关于socekt的内核机制原理,请参阅另一篇文章

http://www.cnblogs.com/LittleHann/p/3875451.html

1. TCP SOCKET通信

server.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>

#define MAXLINE 4096

int main(int argc, char** argv)
{
    int listenfd, connfd;
    /*
    1. sockaddr_in: 用来指明地址信息
    \linux-2.6.32.63\include\linux\in.h
    struct sockaddr_in 
    { 
        1. sin_family指代协议族 
            1) AF_UNIX, AF_LOCAL: Local communication: unix
            2) AF_INET: IPv4 Internet protocols: ip(大多数情况下都是IPV4的socket,所以大多数情况下都是AF_INET)
            3) AF_INET6: IPv6 Internet protocols: ipv6
            4) AF_IPX: IPX: Novell protocols
            5) AF_NETLINK: Kernel user interface device: netlink
            6) AF_X25: ITU-T X.25 / ISO-8208 protocol: x25
            7) AF_AX25: Amateur radio AX.25 protocol
            8) AF_ATMPVC: Access to raw ATM PVCs
            9) AF_APPLETALK: Appletalk: ddp 
            10) AF_PACKET: Low level packet interface: packet 
        sa_family_t     sin_family; 

        //2. sin_port存储端口号(需要使用网络字节顺序的数值对齐进行赋值)  
        __be16      sin_port; 

        /*
        3. sin_addr存储IP地址(需要使用网络字节顺序的数值对齐进行赋值),使用in_addr这个数据结构
        struct in_addr 
        {
            __be32  s_addr;
        };
        */
        struct in_addr  sin_addr;    

        /*
        4.  __pad是为了让sockaddr与sockaddr_in两个数据结构保持大小相同而保留的空字节
     \linux-2.6.32.63\include\linux\socket.h
        struct sockaddr
        {
            sa_family_t sa_family;   
            char        sa_data[14];
        }; 
*/ unsigned char __pad[__SOCK_SIZE__ - sizeof(short int) - izeof(unsigned short int) - sizeof(struct in_addr)]; }; /**/ struct sockaddr_in servaddr; char buff[4096]; int n; /* int socket(int domain, int type, int protocol); 1. domain 指定socket通信的域,和sockaddr_in.sin_family的意义是一样的 1) AF_UNIX, AF_LOCAL: Local communication: unix(进程间socket通信就是用这个domain) 2) AF_INET: IPv4 Internet protocols: ip(大多数情况下都是IPV4的socket,所以大多数情况下都是AF_INET) 3) AF_INET6: IPv6 Internet protocols: ipv6 4) AF_IPX: IPX: Novell protocols 5) AF_NETLINK: Kernel user interface device: netlink 6) AF_X25: ITU-T X.25 / ISO-8208 protocol: x25 7) AF_AX25: Amateur radio AX.25 protocol 8) AF_ATMPVC: Access to raw ATM PVCs 9) AF_APPLETALK: Appletalk: ddp 10) AF_PACKET: Low level packet interface: packet 2. type 指定通信方式 1) SOCK_STREAM(TCP使用) Provides sequenced, reliable, two-way, connection-based byte streams. An out-of-band data transmission mechanism may be supported. 2) SOCK_DGRAM(UDP使用) Supports datagrams (connectionless, unreliable messages of a fixed maximum length). 3) SOCK_SEQPACKET Provides a sequenced, reliable, two-way connection-based data transmission path for datagrams of fixed maximum length; a consumer is required to read an entire packet with
each input system call. 4) SOCK_RAW Provides raw network protocol access. 5) SOCK_RDM Provides a reliable datagram layer that does not guarantee ordering. 6) SOCK_PACKET Obsolete and should not be used in new programs 3. protocol 大多数情况每种domain下只有一种支持的协议protocol,所以这个字段大多数情况下是0
*/ if( (listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1 ) { printf("create socket error: %s(errno: %d)/n",strerror(errno),errno); exit(0); } memset(&servaddr, 0, sizeof(servaddr)); //sin_family指代协议族,在socket编程中只能是AF_INET servaddr.sin_family = AF_INET; /* 在设置IP、PORT的时候需要注意将"主机字节序"转换为可以在网络上传输的"网络字节序" 字节序,顾名思义字节的顺序,就是大于一个字节类型的数据在内存中的存放顺序,一个字节的数据没有顺序的意义 1. 主机字节序(Little-Endian 小端字节序) 低位字节排放在内存的低地址端,高位字节排放在内存的高地址端 2. 网络字节序(Big-Endian 大端字节序) 高位字节排放在内存的低地址端,低位字节排放在内存的高地址端 4个字节的32 bit值以下面的次序传输:首先是0~7bit,其次8~15bit,然后16~23bit,最后是24~31bit 为了进行转换 bsd socket 提供了转换的函数 有下面四个API 1. htons: 把unsigned short类型从主机序转换到网络序 2. htonl: 把unsigned long类型从主机序转换到网络序 3. ntohs: 把unsigned short类型从网络序转换到主机序 4. ntohl: 把unsigned long类型从网络序转换到主机序 (在使用little endian的系统中 这些函数会把字节序进行转换、在使用big endian类型的系统中 这些函数会定义成空宏) */ /* 在设置网络IP的时候需要调用htonl()将"主机字节顺序(小端字节序 little-endian)"转换为"网络字节顺序(大端字节序 big-endian)" 有以下几种设置IP地址的方式 1. 将字符串点数格式地址转化成NBO inet_aton("132.241.5.10", &myaddr.sin_addr); 2. 将字符串点数格式地址转化成无符号长整型(unsigned long s_addr s_addr;) myaddr.sin_addr.s_addr = inet_addr("132.241.5.10"); 3. htons、htonl(Host to Network Short/Long) myaddr.sin_addr.s_addr = htons(INADDR_ANY); myaddr.sin_addr.s_addr = htonl(INADDR_ANY); myaddr.sin_addr.s_addr = INADDR_ANY; 注意!! 1. htons/l和ntohs/l等数字转换都不能用于地址转换,因为地址都是点数格式 2. 地址只能采用数字/字符串转换如inet_aton,inet_ntoa; 3. 唯一可以用于地址转换的htons是针对INADDR_ANY */ servaddr.sin_addr.s_addr = htonl(INADDR_ANY); //在设置端口的时候需要调用htons()将"主机字节顺序(小端字节序 little-endian)"转换为"网络字节顺序(大端字节序 big-endian)" servaddr.sin_port = htons(6666); /* sockaddr和sockaddr_in的相互关系 一般先把sockaddr_in变量赋值后,强制类型转换后传入用sockaddr做参数的函数 1. sockaddr_in用于socket定义和赋值 2. sockaddr用于函数参数 */ /* int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); bind()函数的作用是把一个地址族中的特定地址赋给socket(可以理解为将设置好的"sockaddr结构体"和设置好的"socket描述符"连接起来) 1. sockfd: socket描述符 2. sockaddr: 设置好的地址信息结构体 3. addrlen: sockaddr的字节数 通常服务器在启动的时候都会绑定一个众所周知的地址(如ip地址+端口号),用于提供服务,客户就可以通过它来接连服务器;而客户端就不用指定,有系统自动分配一个端口号和自身的ip地址组合。这就是为什么通常服务器端在
listen之前会调用bind(),而客户端就不会调用,而是在connect()时由系统随机生成一个
*/ if( bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) == -1) { printf("bind socket error: %s(errno: %d)/n",strerror(errno),errno); exit(0); } /* int listen(int sockfd, int backlog); 1. sockfd 要监听的socket描述字 2. backlog 设置相应socket可以排队的最大连接个数 socket()函数创建的socket默认是一个主动类型的(要去向别人发起连接的),listen函数将socket变为被动类型的,等待客户的连接请求 */ if( listen(listenfd, 10) == -1) { printf("listen socket error: %s(errno: %d)/n",strerror(errno),errno); exit(0); } printf("======waiting for client's request======/n"); while(1) { /* int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); 1. sockfd sockfd为服务器的socket描述字,是服务器开始调用socket()函数生成的,称为监听socket描述字,一个服务器通常通常仅仅只创建一个监听socket描述字 2. addr 指向struct sockaddr *的指针,用于返回客户端的协议地址 3. addrlen 协议地址的长度 如果accpet成功,那么其返回值是由内核自动生成的一个全新的描述字,代表与返回客户的TCP连接。它在该服务器的生命周期内一直存在,内核为每个由服务器进程接受的客户连接创建了一个已连接socket描述字,当服务
器完成了对某个客户的服务,相应的已连接socket描述字就被关闭
*/ if( (connfd = accept(listenfd, (struct sockaddr*)NULL, NULL)) == -1) { printf("accept socket error: %s(errno: %d)",strerror(errno),errno); continue; } /* 当服务器与客户已经建立好连接之后。可以调用网络I/O进行读写操作了,即实现了网咯中不同进程之间的通信 我们知道,linux下所有的东西都是文件,同样,对socket的操作就是对文件的操作,网络I/O操作有下面几组: 1. read()/write() 1) ssize_t read(int fd, void *buf, size_t count); 2) ssize_t write(int fd, const void *buf, size_t count); 2. recv()/send() 1) ssize_t send(int sockfd, const void *buf, size_t len, int flags); 2) ssize_t recv(int sockfd, void *buf, size_t len, int flags); 3. readv()/writev():TCP常用 1) ssize_t readv(int fd, const struct iovec *iov, int iovcnt); 2) ssize_t writev(int fd, const struct iovec *iov, int iovcnt); 4. recvmsg()/sendmsg() 1) ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags); 2) ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags); 5. recvfrom()/sendto(): UDP常用 1) ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen); 2) ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen); */ n = recv(connfd, buff, MAXLINE, 0); buff[n] = '/0'; printf("recv msg from client: %s/n", buff); close(connfd); } close(listenfd); }

client.c

#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <unistd.h>
 
//以下头文件是为了使样例程序正常运行
#include <string.h>
#include <stdio.h>
#include <stdlib.h>

int main(int argc, char const *argv[])
{
    struct sockaddr_in pin;

    /*
    struct hostent 
    {
        char  *h_name;      //official name of host      
        char **h_aliases;   //alias list 
        int    h_addrtype;  //host address type  
        int    h_length;    //length of address  
        char **h_addr_list; //list of addresses 
    }
    #define h_addr h_addr_list[0]   //for backward compatibility  
    */
    struct hostent *nlp_host;
    int sd; 
    char host_name[256];
    int port;
     
    /*
    初始化主机名和端口。主机名可以是IP,也可以是可被解析的名称
    1. strcpy(host_name,"115.239.210.27");
    //strcpy(host_name,"www.baidu.com");
strcpy(host_name,"1945096731");
    */
    strcpy(host_name, "127.0.0.1");
    port = 8000;
     
    /* 
    有以下几种设置IP地址的方式
    1. 将字符串点数格式地址转化成NBO
    inet_aton("132.241.5.10", &myaddr.sin_addr);
    2. 将字符串点数格式地址转化成无符号长整型(unsigned long s_addr s_addr;)
    myaddr.sin_addr.s_addr = inet_addr("132.241.5.10");
    3. htons、htonl(Host to Network Short/Long)
    myaddr.sin_addr.s_addr = htons(INADDR_ANY);  
    myaddr.sin_addr.s_addr = htonl(INADDR_ANY);  
    myaddr.sin_addr.s_addr = INADDR_ANY; 
    4. 使用gethostbyname进行IP设置
    使用gethostbyname(),可以针对各种不同的畸形域名、IP、进行统一的赋值,在gethostbyname()中会自动对输入值进行智能判断
    http://lcx.cc/?i=4409 
    struct hostent *nlp_host; 
    name == IP地址、域名、10进制/16进制的IP地址
    nlp_host = gethostbyname(host_name);
    myaddr.sin_addr.s_addr = ((struct in_addr *)(nlp_host->h_addr))->s_addr;
    */
    while ((nlp_host = gethostbyname(host_name))==0)
    {
        printf("Resolve Error!\n");
    } 

    bzero(&pin,sizeof(pin));
    pin.sin_family = AF_INET;                 //AF_INET表示使用IPv4 
    pin.sin_addr.s_addr = ((struct in_addr *)(nlp_host->h_addr))->s_addr;
    pin.sin_port = htons(port);
     
    /*
    建立tcp socket连接
    */
    sd = socket(AF_INET, SOCK_STREAM ,0);
     
    /*
    建立连接
    int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
    建立tcp socket 
    1. sockfd
    要监听的socket描述字
    2. addr
    待连接目标server的信息
    3. addrlen
    相应的addr的字节数
    
    客户端通过调用connect函数来建立与TCP服务器的连接
    */
    while (connect(sd,(struct sockaddr*)&pin,sizeof(pin))==-1)
    {
      printf("Connect Error!\n");
    }
    return 0;
}

2. UDP SOCKET通信

server.c

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>

#define ERR_EXIT(m) \
    do { \
        perror(m); \
        exit(EXIT_FAILURE); \
    } while (0)

void echo_ser(int sock)
{
    char recvbuf[1024] = {0};
    struct sockaddr_in peeraddr;
    socklen_t peerlen;
    int n;

    while (1)
    {

        peerlen = sizeof(peeraddr);
        memset(recvbuf, 0, sizeof(recvbuf));
        //ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
        n = recvfrom(sock, recvbuf, sizeof(recvbuf), 0, (struct sockaddr *)&peeraddr, &peerlen);
        if (n == -1)
        { 
            if (errno == EINTR)
            {
                continue;
            }  
            ERR_EXIT("recvfrom error");
        }
        else if(n > 0)
        { 
            fputs(recvbuf, stdout);
            //ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
            sendto(sock, recvbuf, n, 0, (struct sockaddr *)&peeraddr, peerlen);
        }
    }
    close(sock);
}

int main(void)
{
    int sock;
    if ((sock = socket(AF_INET, SOCK_DGRAM, 0)) < 0)
    {
        ERR_EXIT("socket error");
    } 

    struct sockaddr_in servaddr;
    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(5188);
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);

    //对于UDP的server来说,bind将指定的ip/port绑定之后,就相当于监听这个端口了(因为UDP是无连接协议)
    if (bind(sock, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0)
    {
        ERR_EXIT("bind error");
    }  
    echo_ser(sock);

    return 0;
}

client.c

#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>

#define ERR_EXIT(m) \
        do \
        { \
                perror(m); \
                exit(EXIT_FAILURE); \
        } while(0)

void echo_cli(int sock)
{
    struct sockaddr_in servaddr;
    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(5188);

    /* 
    有以下几种设置IP地址的方式
    1. 将字符串点数格式地址转化成NBO
    inet_aton("132.241.5.10", &myaddr.sin_addr);
    2. 将字符串点数格式地址转化成无符号长整型(unsigned long s_addr s_addr;)
    myaddr.sin_addr.s_addr = inet_addr("132.241.5.10");
    3. htons、htonl(Host to Network Short/Long)
    myaddr.sin_addr.s_addr = htons(INADDR_ANY);  
    myaddr.sin_addr.s_addr = htonl(INADDR_ANY);  
    myaddr.sin_addr.s_addr = INADDR_ANY; 
    4. 使用gethostbyname进行IP设置
    使用gethostbyname(),可以针对各种不同的畸形域名、IP、进行统一的赋值,在gethostbyname()中会自动对输入值进行智能判断
    http://lcx.cc/?i=4409 
    struct hostent *nlp_host; 
    name == IP地址、域名、10进制/16进制的IP地址
    nlp_host = gethostbyname(host_name);
    myaddr.sin_addr.s_addr = ((struct in_addr *)(nlp_host->h_addr))->s_addr;
    */
    servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");

    int ret;
    char sendbuf[1024] = {0};
    char recvbuf[1024] = {0};
    while (fgets(sendbuf, sizeof(sendbuf), stdin) != NULL)
    { 
        //ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
        sendto(sock, sendbuf, strlen(sendbuf), 0, (struct sockaddr *)&servaddr, sizeof(servaddr));

        //ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
        ret = recvfrom(sock, recvbuf, sizeof(recvbuf), 0, NULL, NULL);
        if (ret == -1)
        {
            if (errno == EINTR)
            {
                continue;
            } 
            ERR_EXIT("recvfrom");
        }

        fputs(recvbuf, stdout);
        memset(sendbuf, 0, sizeof(sendbuf));
        memset(recvbuf, 0, sizeof(recvbuf));
    }

    close(sock); 
}

int main(void)
{
    int sock;
    //建立udp socket
    if ((sock = socket(AF_INET, SOCK_DGRAM, 0)) < 0)
    {
        ERR_EXIT("socket");
    }  
    echo_cli(sock);

    return 0;
}

3. 本机进程间SOCKET通信

server.c

//s_unix.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/un.h> 

#define UNIX_DOMAIN "/tmp/UNIX.domain"

int main(void)
{
    socklen_t clt_addr_len;
    int listen_fd;
    int com_fd;
    int ret;
    int i;
    static char recv_buf[1024]; 
    int len;

    /*
    这是在使用socket进行进程间通信时会使用到的数据结构
    struct sockaddr_un 
    { 
        // PF_UNIX或AF_UNIX  
        sa_family_t sun_family; 

        // 路径名  
        char sun_path[UNIX_PATH_MAX]; 
    };
    */
    struct sockaddr_un clt_addr;
    struct sockaddr_un srv_addr;

    /*
    创建一个进程间双向可靠通信socket(AF_UNIX)
    */
    listen_fd = socket(AF_UNIX, SOCK_STREAM, 0);
    if(listen_fd < 0)
    {
        perror("cannot create communication socket");
        return 1;
    }  
    
    //set server addr_param
    srv_addr.sun_family = AF_UNIX;
    strncpy(srv_addr.sun_path, UNIX_DOMAIN, sizeof(srv_addr.sun_path)-1);
    unlink(UNIX_DOMAIN);
    /*
    bind sockfd & addr
    1. struct sockaddr是通用的套接字地址
    2. struct sockaddr_in则是internet环境下套接字的地址形式
    3. sockaddr_un是UNIX进程间通信使用的套接字的地址形式
    它们长度一样,都是16个字节。是并列结构,指向sockaddr_in结构的指针也可以指向sockaddr、sockaddr_un
    4. 一般情况下,需要把sockaddr_in、sockaddr_un结构强制转换成sockaddr结构再传入系统调用函数中
    */
    ret = bind(listen_fd,(struct sockaddr*)&srv_addr,sizeof(srv_addr));
    if(ret==-1)
    {
        perror("cannot bind server socket");
        close(listen_fd);
        unlink(UNIX_DOMAIN);
        return 1;
    }
    //listen sockfd 
    ret=listen(listen_fd,1);
    if(ret==-1)
    {
        perror("cannot listen the client connect request");
        close(listen_fd);
        unlink(UNIX_DOMAIN);
        return 1;
    }

    //have connect request use accept
    len = sizeof(clt_addr);
    com_fd = accept(listen_fd,(struct sockaddr*)&clt_addr, &len);
    if(com_fd < 0)
    {
        perror("cannot accept client connect request");
        close(listen_fd);
        unlink(UNIX_DOMAIN);
        return 1;
    }
    //read and printf sent client info
    printf("/n=====info=====/n");
    for(i = 0; i < 4; i++)
    {
        memset(recv_buf,0,1024);
        //ssize_t read(int fd, void *buf, size_t count);
        int num = read(com_fd, recv_buf, sizeof(recv_buf));
        printf("Message from client (%d)) :%s/n",num,recv_buf);  
    }
    close(com_fd);
    close(listen_fd);
    unlink(UNIX_DOMAIN);
    return 0;
}

client.c

//c_unix.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/un.h>

#define UNIX_DOMAIN "/tmp/UNIX.domain"

int main(void)
{
    int connect_fd;
    int ret;
    char snd_buf[1024];
    int i;
    static struct sockaddr_un srv_addr;
    //creat unix socket for ipc communication
    connect_fd = socket(AF_UNIX, SOCK_STREAM, 0);
    if(connect_fd<0)
    {
        perror("cannot create communication socket");
        return 1;
    }   
    srv_addr.sun_family=AF_UNIX;
    strcpy(srv_addr.sun_path, UNIX_DOMAIN);
    //connect server
    ret = connect(connect_fd, (struct sockaddr*)&srv_addr, sizeof(srv_addr));
    if(ret==-1)
    {
        perror("cannot connect to the server");
        close(connect_fd);
        return 1;
    }
    memset(snd_buf, 0, 1024);
    strcpy(snd_buf, "message from client");
    //send info server
    for(i=0;i<4;i++)
    {
        //ssize_t write(int fd, const void *buf, size_t count);
        write(connect_fd, snd_buf, sizeof(snd_buf));
    } 
    close(connect_fd);
    return 0;
}

Relevant Link:

http://kenby.iteye.com/blog/1149534
http://linux.die.net/man/7/socket
http://www.beej.us/guide/bgnet/output/html/multipage/sockaddr_inman.html
http://www.gta.ufrj.br/ensino/eel878/sockets/sockaddr_inman.html
http://www.cnblogs.com/skynet/archive/2010/12/12/1903949.html
http://blog.sina.com.cn/s/blog_6151984a0100etj1.html
http://blog.csdn.net/hguisu/article/details/7445768
http://www.ibm.com/developerworks/cn/education/linux/l-sock/l-sock.html
http://www.cnblogs.com/hnrainll/archive/2011/04/24/2026432.html
《深入linux内核架构》 12章

0x4: System V通信机制(System V IPC Mechanisms)

Linux使用System V(SysV)引入的机制,来支持用户进程的进程间通信和同步,内核通过系统调用提供了各种例程,使得用户库(通常是C标准库)能够实现所需的操作
Linux下的system V通信机制包括

1. 信号量(Semaphores)
2. 消息队列(Message Queues)
3. 共享内存(Shared Memory)

System V Unix的3种进程间通信(IPC)机制虽然是3种不同的通信概念,但是却有一个共同点:

1. 它们都使用了全系统范围的资源
2. 可以由多个进程同时共享

在各个独立进程能够访问SysV IPC对象之前,IPC对象必须在系统内唯一标识。因此,每种IPC结构在创建时分配了一个号码,只要知道这个魔数的各个程序,都能够直接访问对应的结构。如果独立的应用程序需要互相通信,则通常需要将该魔数永久地编译到程序代码中。一种备选的方案是动态地产生一个保证唯一的魔数,标准库提供了几个完成此工作的函数

在访问IPC对象时,系统采用了基于文件访问权限的权限系统。每个IPC对象都有一个uid、gid,依赖于产生IPC对象的程序运行在何种UID/GID之下。读写权限在初始化时分配,类似于普通文件,这些控制了3种不同用户类别的访问: 所有者、组、其他
要创建一个授予所有可能访问权限的IPC对象(所有者、组、其他用户都有读写权限),则必须指定标志0666

system V机制是Linux下进行特定格式的大数据量的IPC的常用手段,关于消息队列(Message Queues)、共享内存(Shared Memory)的异同点如下

1. 共享内存
    1) 共享内存是每个进程都有权力去修改映射在其内存空间内的全部数据,它关注的是资源的实时共享
    2) 共享内存如果资源控制做的不好,可能就会产生进程A写入的数据,修改了刚刚进程B写入的数据

2. 消息队列
    1) 消息队列则是真正的生产者-消费者模式的具体实现方式
    2) 资源绝对的互斥,进程A向进程C发送的消息不会被任何其他进程修改

1. 信号量(Semaphores)
System V信号量在"\linux-2.6.32.63\ipc\sem.c"中实现,对应的头文件是<sem.h>
一个System V信号量是指一整套信号量,可以允许几个操作同时进行(尽管从用户角度看它们是原子的)。当然也可以请求只有一个信号量的信号量集合,并定义函数模拟原始信号量的简单操作

#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>

#define SEMKEY 1234L    //标识符
#define PERMS 0666    //访问权限: rwrwrw

/*
每个sembuf结构体由3个成员组成
1. 选择信号量集合中需要操作的信号量
2. 指定所需的操作
    1) 0: 表示一直等待,直到信号量的值到达0
    2) 正数: 表示将该值加到信号量(对应于释放资源)
    3) 负数: 表示从当前信号量值减去其(绝对)值,如果其绝对值小于信号量的值(用户申请资源)
3. 是一个标志,用于精细控制操作
*/
struct sembuf op_down[1] = {0, -1, 0};
struct sembuf op_up[1] = {0, 1, 0};

int semid = -1;        //信号量ID
int res;        //信号量操作的结果

void init_sem()
{
    //该程序可能有几个副本同时运行,因此需要检测信号量是否已经存在,如果不存在则进行创建
    semid = semget(SEMKEY, 0, IPC_CREAT | PERMS);
    if(semid < 0)
    {
        printf("Create the semaphore\n");
        
        /*
        使用一个持久的魔数(1234)创建了一个新的信号量,以便在系统内建立标识
        创建一个只有一个信号量的信号集合
        */
        semid = semget(SEMKEY, 1, IPC_CREAT | PERMS);
        if(semid < 0)
        {
            printf("Couldn't create the semaphore\n");
            exit(-1);
        }

        //初始化为1
        res = semctl(semid, 0, SETVAL, 1);
    }
}

//执行down操作
void down()
{
    res = semop(semid, &op_down[0], 1);
}

//执行up操作
void up()
{
    res = semop(semid, &op_up[0], 1);
}

int main()
{
    init_sem();

    /*
    TODO: 正常的程序代码
    */

    printf("Before critical code\n");
    down();
    //临界区代码
    printf("In critical code\n");
    sleep(10);
    up();

    //其余代码
    return 0;
} 

使用只有一个信号量的信号集合,模拟了一种最经典的信号量行为,编译完成后,依次启动两个本进程

可以看到,2个进程遵循了信号量的竞态条件,依次进入了临界区执行了指令

数据结构: 内核使用了几个数据结构来描述所有注册信号量的当前状态,并建立了一个网状结构。它们不仅负责管理信号量及其特征(值、读写权限等),还负责通过等待列表将信号量与等待进程关联起来

http://www.cnblogs.com/LittleHann/p/3865490.html
//搜索: 9. 进程间通信(IPC)相关数据结构

权限检查

IPC对象的保护机制,与Linux普通文件的对象相同

1. 对对象的所有者: 读/写/执行
2. 对对象的所有者所在的组: 读/写/执行
3. 对其他用户: 读/写/执行

ipcperms()负责检查对任意IPC对象的某种操作是否有权限进行

\source\linux-2.6.32.63\ipc\util.c

/**
 *    ipcperms    -    check IPC permissions
 *    @ipcp: IPC permission set
 *    @flag: desired permission set.
 *
 *    Check user, group, other permissions for access
 *    to ipc resources. return 0 if allowed
 */
 
int ipcperms (struct kern_ipc_perm *ipcp, short flag)
{    
    /* flag will most probably be 0 or S_...UGO from <linux/stat.h> */
    uid_t euid = current_euid();
    /*
    1. requested_mode: 所请求的权限位
    2. granted_mode: 初始值包含了IPC对象的权限位,根据当前操作执行者的不同(用户自身、所属组、其他人),分别将granted_mode右移适当数目的bit位,使得低3位刚好是表示权限的3个bit位
    */
    int requested_mode, granted_mode;

    audit_ipc_obj(ipcp);
    requested_mode = (flag >> 6) | (flag >> 3) | flag;
    granted_mode = ipcp->mode;
    if (euid == ipcp->cuid || euid == ipcp->uid)
        granted_mode >>= 6;
    else if (in_group_p(ipcp->cgid) || in_group_p(ipcp->gid))
        granted_mode >>= 3;
    /* 
    is there some bit set in requested_mode but not in granted_mode? 
    1. DAC访问权限检查
    2. IPC对象属主检查
    */
    if ((requested_mode & ~granted_mode & 0007) && !capable(CAP_IPC_OWNER))
        return -1;

    //LSM对IPC访问权限的挂载点
    return security_ipc_permission(ipcp, flag);
}

/source/kernel/capability.c

bool capable(int cap)
{    
    /*
    For the purpose of performing permission checks, traditional UNIX implementations distinguish two categories of processes: privileged processes (whose effective user ID is 0, referred to as superuser or root), and unprivileged processes (whose effective UID is nonzero).
    Privileged processes bypass all kernel permission checks, while unprivileged processes are subject to full permission checking based on the process's credentials (usually: effective UID, effective GID, and supplementary group list).
    */
    return ns_capable(&init_user_ns, cap);
}

实现系统调用

所有对信号量的操作都使用一个名为ipc的系统调用执行,该调用不仅用于信号量,也用于操作消息队列和共享内存,其第一个参数用于将实际工作委托给其他函数
/source/include/linux/syscalls.h

asmlinkage long sys_ipc(unsigned int call, int first, unsigned long second, unsigned long third, void __user *ptr, long fifth);

用于信号量的函数如下所示

1. SEMCTL: 执行信号量操作,并由sys_semctl实现
2. SEMGET: 读取信号量ID,相关的实现由sys_semget提供
3. SEMOP、SEMTIMEDOP: 负责增加和减少信号量值,后者可以指定超时时间限制

Relevant Link:

http://man7.org/linux/man-pages/man7/capabilities.7.html
http://lxr.free-electrons.com/source/kernel/capability.c#L424
http://lxr.free-electrons.com/source/include/uapi/linux/capability.h#L204

2. 共享内存(Shared Memory)

共享内存是进程间通信的一个概念,从用户和内核的角度来看,它的实现机制上和消息队列、信号量类似,并没有本质的不同

1. 应用程序请求的IPC对象,可以通过魔数和当前命名空间的内核部分ID访问到
2. 对共享内存的访问,可能受到权限系统的限制
3. 可以使用系统调用分配与IPC对象关联的内存,具备适当授权的所有进程,都可以访问这块内存

smd_ids全局变量的entries数组中保存了kern_ipc_perm和shmid_kernel的组合,以便管理IPC对象的访问权限。对每个共享内存对象都创建一个伪文件,通过shm_file连接到shmid_kernel的实例。
内核使用shm_file->f_mapping指针访问地址空间对象(struct address_space),用于创建匿名映射。还需要设置所涉及各进程的页面,使得各个进程都能够访问与该IPC对象相关的内存区域

1. 多线程生产者
2. 多线程消费者(也可以单个消费者)
3. 共享内存IPC
    1) 只用一把锁
    2) 在共享内存区域的头部设置一个定长的字段,用于表示当前共享内存区的读写指针偏移:offset
    3) 生产者写之前获取锁,从offset开始,往里写数据
    4) 消费者取之前获取锁,从buffer直接取走所有数据(start ~ offset),一次全部flush,也可以多线程去获取
    5) 消费者flush数据后,将共享内存区域开头的offset字段清零

3. 消息队列(Message Queues)

消息队列(message queues)是Linux内核提供的一种通信机制。消息队列也称为报文队列,消息队列是随内核持续的,只有在内核重起或显示删除一个消息队列时,该消息队列才会真正删除。使用消息队列相比于管道的优点在于

1. 异步性
    1) 一个进程向消息队列写入消息之前,并不需要某个进程在该队列上等待该消息的到达
    2) 而管道和FIFO是相反的,进程向其中写消息时,管道和FIFO必需已经打开来读,否则写进程就会阻塞(默认情况下)

2. IPC的持续性不同
    1) 管道和FIFO是随进程的持续性,当管道和FIFO最后一次关闭发生时,仍在管道和FIFO中的数据会被丢弃
    2) 消息队列是随内核的持续性,即一个进程向消息队列写入消息后,然后终止,另外一个进程可以在以后某个时刻打开该队列读取消息。只要内核没有重新自举,消息队列没有被删除 

产生消息并将其写到队列的进程通常称之为发送者,而一个或多个其他进程(逻辑上称为接收者)则从队列获取消息
消息队列是消息的链接表,存放在内核中并由"消息队列标识符"标识,每个消息队列有一个队列头,称为struct msg_queue,同一个编号(消息队列标识符)的消息按先进先出次序处理,放置在队列开始的消息将首先读取,在消息已经被读取后,内核将其从队列删除,即使几个进程在同一个信道上监听,每个消息仍然只能由一个进程读取

消息队列也是使用和信号量相同的基础数据结构实现的(实际上,整个SysV机制的底层都使用了同一套基础数据结构)

1. 起始点是当前命名空间的适当的ipc_ids实例
2. 内部的ID号形式上关联到kern_ipc_perm实例
3. 在消息队列的实现中,需要通过类型转换获得不同的数据类型: struct msq_queue

关于消息队列的相关数据结构的知识,请参阅另一篇文章

http://www.cnblogs.com/LittleHann/p/3865490.html
//搜索:0x6: struct msg_queue

code example

client.c

#include <string.h>
#include <stdlib.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <sys/stat.h>

#define MSG_FILE "server.c"
#define BUFFER 255
#define PERM S_IRUSR|S_IWUSR

struct msgtype 
{
    long mtype;
    char buffer[BUFFER+1];
};

int main(int argc,char **argv)
{
    struct msgtype msg;
    key_t key;
    int msgid;
    if(argc != 2)
    {
        printf("Usage:%s string\n\a", argv[0]);
        exit(1);
    }

    /*
    函数原型:
    key_t ftok( const  char * pathname , int   proj_id );
    参数:
     1. pathname: 指定的文件名(该文件必须是存在而且可以访问的)
     2. id是子序号,虽然为int,但是只有8个比特被使用(0-255)
    返回值:
    1. 成功时候返回 key_t 类型的key值
    2. 失败返回-1
    */
    if((key = ftok(MSG_FILE, 'a')) == -1)
    {
        printf("Creat Key Error:%d\n", errno);
        exit(1);
    }

    /*
    函数原型: 
    int msgget ( key_t  key , int  msgflg );
    函数描述:建立消息队列
    参数:
    1. key: 消息队列对象的关键字(key),函数将它与已有的消息队列对象的关键字进行比较来判断消息队列对象是否已经创建
    2. msgflg: 决定函数进行的具体操作是,它可以取下面的几个值:
        1) IPC_CREAT :如果消息队列对象不存在,则创建之,否则则进行打开操作;
        2) IPC_EXCL:和IPC_CREAT 一起使用(用"|"连接),如果消息对象不存在则创建之,否则产生一个错误并返回 
    返回值:
        1) 成功时返回队列ID
        2) 失败返回-1
        3) 错误原因存于error 
            EEXIST (Queue exists, cannot create)
            EIDRM (Queue is marked for deletion)
            ENOENT (Queue does not exist)
            ENOMEM (Not enough memory to create queue)
            ENOSPC (Maximum queue limit exceeded)
    */
    if((msgid = msgget(key, PERM)) == -1)
    {
        printf("Creat Message Error:%d\n", errno);
        exit(1);
    }
    msg.mtype=1;
    strncpy(msg.buffer, argv[1], BUFFER);

    /*
    函数原型:
    int  msgsnd ( int msgid ,  struct msgbuf*msgp , int msgsz, int msgflg );
    参数说明:
    1. msgid: 消息队列对象的标识符(由msgget()函数得到)
    2. msgp: 指向要发送的消息所在的内存
    3. msgsz: 要发送信息的长度(字节数),可以用以下的公式计算: msgsz = sizeof(struct mymsgbuf) - sizeof(long);
    4. msgflg: 控制函数行为的标志,可以取以下的值:
        1) 0: 忽略标志位 
        2) IPC_NOWAIT: 如果消息队列已满,消息将不被写入队列,控制权返回调用函数的线程。如果不指定这个参数,线程将被阻塞直到消息被可以被写入
        从最佳实践的角度来说,msgflg最好设置为"IPC_NOWAIT",因为考虑到内存消耗应该有一个最大上限,如果当前数据生产者的产生速度过快,
        在这种高压的情况下,丢弃过载的数据包是合理的
    返回值:
    1. 0 on success
       2. -1 on error
       错误代码errno如下
       1. EAGAIN (queue is full, and IPC_NOWAIT was asserted)
    2. EACCES (permission denied, no write permission)
    3. EFAULT (msgp address isn't accessable - invalid)
    4. EIDRM  (The message queue has been removed)
    5. EINTR  (Received a signal while waiting to write)
    6. EINVAL (Invalid message queue identifier, nonpositive message type, or invalid message size) 
    7. ENOMEM (Not enough memory to copy message buffer)
    */
    msgsnd(msgid, &msg, sizeof(struct msgtype), 0);
    memset(&msg, '\0', sizeof(struct msgtype));

    /*
    函数定义:
    int  msgrcv( int  msgid, struct msgbuf* msgp, int msgsz, long msgtyp, int msgflg);
    参数:
    1. msgid: 消息队列对象的标识符(由msgget()函数得到)
    2. msgp: 指向要发送的消息所在的内存
    3. msgsz: 要发送信息的长度(字节数),可以用以下的公式计算: msgsz = sizeof(struct mymsgbuf) - sizeof(long);
    4. msgtyp: 指定了函数从队列中所取的消息的类型。函数将从队列中搜索类型与之匹配的消息并将之返回,取值如下
        1)  msgtyp = 0: 不分类型,直接返回消息队列中的第一项
         2) msgtyp > 0: 返回第一项 msgtyp与 msgbuf结构体中的mtype相同的信息
        3) msgtyp <0: 返回第一项 mtype小于等于msgtyp绝对值的信息
        4) 这里有一个例外。如果mtype的值是零的话,函数将不做类型检查而自动返回队列中的最旧的消息
    5. msgflg: 控制函数行为的标志,取值可以是:
        1) 0: 表示忽略;
        2) IPC_NOWAIT: 如果消息队列为空,则返回一个ENOMSG,并将控制权交回调用函数的进程
        3) IPC_NOERROR: 若信息长度超过参数msgsz,则截断信息而不报错。
        4) 如果不指定这个参数,那么进程将被阻塞直到函数可以从队列中得到符合条件的消息为止
    返回值:
    1. 成功时返回所获取信息的长度
    2. 失败返回-1
    错误信息存于error
    1. E2BIG (Message length is greater than msgsz,no MSG_NOERROR)
    2. EACCES (No read permission)
    3. EFAULT (Address pointed to by msgp is invalid)
    4. EIDRM (Queue was removed during retrieval)
    5. EINTR (Interrupted by arriving signal)
    6. EINVAL (msgqid invalid, or msgsz less than 0)
    7. ENOMSG (IPC_NOWAIT asserted, and no message exists in the queue to satisfy the request)
    8. 如果一个client 正在等待消息的时候队列被删除,EIDRM 就会被返回
    8. 如果进程在阻塞等待过程中收到了系统的中断信号,EINTR 就会被返回  
    */
    msgrcv(msgid, &msg, sizeof(struct msgtype), 2, 0);
    printf("Client receive:%s\n", msg.buffer);
    exit(0);
} 

server.c

#include <string.h>
#include <stdlib.h>
#include <errno.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/stat.h>
#include <sys/msg.h>

#define MSG_FILE "server.c"
#define BUFFER 255
#define PERM S_IRUSR|S_IWUSR

struct msgtype 
{
    long mtype;
    char buffer[BUFFER+1];
};

int main()
{
    struct msgtype msg;
    key_t key;
    int msgid;
    if((key = ftok(MSG_FILE, 'a')) == -1)
    {
        printf("Creat Key Error:%d\n", errno);
        exit(1);
    }
    if((msgid = msgget(key, PERM|IPC_CREAT|IPC_EXCL)) == -1)
    {
        printf("Creat Message Error:%d\n", errno);
        exit(1);
    }
    while(1)
    {
        msgrcv(msgid, &msg, sizeof(struct msgtype), 1, 0);
        printf("Server Receive:%s\n", msg.buffer);
        msg.mtype=2;
        msgsnd(msgid, &msg, sizeof(struct msgtype), 0);
    }
    exit(0);
} 

Relevant Link:

http://zh.wikipedia.org/zh-cn/%E6%B6%88%E6%81%AF%E9%98%9F%E5%88%97
http://lobert.iteye.com/blog/1743256
http://blog.csdn.net/anonymalias/article/details/9799645
http://www.cnblogs.com/biyeymyhjob/archive/2012/08/04/2623323.html
http://www.cnblogs.com/biyeymyhjob/archive/2012/11/04/2753535.html
http://blog.csdn.net/ljianhui/article/details/10287879
http://www.cnblogs.com/lpshou/p/3145651.html
http://www.cnblogs.com/polestar/archive/2012/04/16/2451202.html
http://www.live-in.org/archives/2226.html

 

3. 多线程并行中的阻塞和同步

多线程程序处于一种多变的环境中,可访问的全局变量和堆数据随时都可能被其他的线程改变,因此多线程程序在并发时数据的一致性变得非常重要,这里就需要明白一个概念:"竞争与原子操作"

当多个线程试图同时访问同一个共享数据时,就会造成很严重的数据不一致问题,造成这个现象的本质原因是

大多数的操作被编译为汇编代码后不止一条指令,而CPU只能保证单条指令的原子性执行(因为CPU只会在每条指令的脉冲下降沿处检测当前是否发生了中断)

从这个角度作为出发点去思考线程间同步的机制,我们会发现本质上我们是在进行"原子化"、或者叫"广义原子化"操作,即我们人为地将某段代码块"包裹"为一段原子形态的操作

实现"原子化"的操作有如下几种

0x1: CPU指令集提供的原子操作(Atomic)

很多CPU体系都提供了一些常用操作的原子指令

1) inc: 自增指令
2) dec: 自减指令

0x2: 操作系统提供的原子操作API

使用这些函数时,windows将保证是原子操作的,因此可以不用担心出现问题

1) InterlockedExchange: 原子地交换两个值
2) InterlockedDecrement: 原子地减少一个值
3) InterlockedIncrement: 原子地增加一个值
4) InterlockedXor: 原子地进行异或操作

但是需要注意的是,尽管原子操作指令非常方便,但是它们仅仅适用于比较简单特定的场合,在复杂的场合下,比如我们要保证一个复杂的数据结构更改的原子性,原子操作API就无法满足我们的要求了。我们需要更加通用的机制,即锁(广义"原子操作")

0x3: 同步与锁

为了避免多个线程同时读写同一个数据而产生不可预料的后果,我们要将各个线程对同一个数据的访问同步(synchronization),即在一个线程访问数据未结束的时候,其他线程不得对同一个数据进行访问,如此,对数据的访问就被原子化了
同步的最常见的方法是使用锁(Lock)。锁是一种非强制机制,每一个线程在访问数据或资源之前首先试图获取(acquire)锁,并在访问结束之后释放(Release)锁。在锁已经被占用的时候试图获取锁时,线程会等待,直到锁重新可用

值得注意的是,操作系统或者程序设计中的这种"锁"是一种学术上的概念,它对应的有很多种实现方式来达到这个"锁"的效果。我们在实际的使用中,我们应该明我们使用的是哪一种锁,以及使用这种锁体现了什么样的思想

从下面开始介绍的,全部都可以称之为"锁"

0x4: 二元信号量(binary semaphore)

这是最简单的一种锁,它只有两种状态

1. 占用
2. 非占用

它适合的业务场景是只能被唯一一个线程独占访问的资源。当二元信号量处于非占用状态时,第一个试图获取该二元信号量的线程会获得该锁,并将二元信号量重置为占用状态,此后其他的所有的试图获取该二元信号量的线程将会等待,直到该锁释放

0x5: 信号量(多元信号量)

对于允许多个线程并发访问的资源,多元信号量简称"信号量(semaphore)",一个初值为N的信号量允许N个线程并发访问,每个线程在获取一次锁之后都能递减一次信号量。线程访问资源的时候首先获取信号量,进行如下操作

1. 在一个进程想要进入关键代码时,它调用down函数,将信号量的值减1
2. 如果信号量的值小于等于0,则进入等待状态,将信号量减1,在进程在信号量上睡眠时,内核将其至于"阻塞状态",且与其他在该信号量上等待的进程一同放到一个等待列表中
3. 如果信号量的值大于等于1,则将信号量值减1,继续执行"临界区代码段"。访问完资源之后,线程释放信号量,进行如下操作
3. 如果信号量的值大于等于0,将信号量的值加1
4. 如果信号量的值小于0(即当前至少有1个线程处于等待状态),唤醒一个等待中的线程

在执行down/up操作时,有一点特别重要

1. 即从应用程序的角度来看,该操作应视为一个原子操作,它不能被调度器调用中断,这意味着竞态条件是无法发生的
2. 从内核的视角来看,查询变量的值和修改变量的值是两个不同的操作,但用户将二者视为一个原子操作
3. 实现down/up的原子操作都需要内核的支持

信号量在用户层可以正常工作,原则上也可以用于解决内核内核的各种锁问题,但事实上不是这样,性能是内核最首先的一个目标,虽然信号量实现起来较容易,但其开销对内核来说过大,这也是内核中提供了许多不同的锁和同步机制的原因

0x6: 互斥量(mutex)

互斥量和二元信号量很类似,资源仅同时允许一个线程访问,但和信号量不同的是,信号量在整个系统可以被任意线程获取并释放,也就是说:

1. 同一个信号量可以被系统中的一个线程获取之后由另一个线程释放
2. 而互斥量则要求释放互斥量的线程只能是获取互斥量的同一个线程

0x7: 临界区(critical section)

临界区是比互斥量更加严格的同步手段。把临界区的锁的获取称之为"进入临界区",而把锁的释放称之为离开临界区。临界区和互斥量与信号量的区别在于

1. 互斥量和信号量在系统的任何进程里都是"可见"的,也就是说,一个进程创建了一个互斥量或信号量,另一个进程试图去获取该锁是合法的,但是有可能发生等待
2. 然后,临界区的作用范围仅限于本进程,其他的进程无法获取该锁
3. 从某种程度上来说,临界区是一种本进程内代码块的互斥同步的机制

0x8: 读写锁(read-write lock)

读写锁的使用场景是一种更加特定的业务场景

1. 对于一段数据,多个线程同时读取"读取"总是没有问题的
2. 但只要有任何一个线程试图对这个数据进行修改,就必须使用同步手段来避免出错

我们来思考读写锁带来的意义,如果我们使用信号量、互斥量、或者临界区中的任何一种来进行同步,尽管可以保证程序正确运行,但对于"读取频繁",而仅仅偶尔写入的场景下,会显得非常低效

读写锁可以避免这个问题,对于同一个锁,读写锁有两种获取方式

1. 共享的(shared)
2. 独占的(exclusive)

下面简单描述读写锁的状态机

1. 当锁处于自由状态时
    1) 试图以任何一种方式获取锁都能成功,并将锁置于对应的状态
2. 如果锁处于共享状态
    1) 其他线程以共享的方式获取锁仍然会成功,此时这个锁分配给了多个线程
    2) 如果其他线程试图以独占的方式获取已经处于共享状态的锁,那么它将必须等待被所有的读线程释放
3. 如果锁处于独占状态
    1) 不论其他线程试图以哪种方式获取锁,锁都将组织任何线程获取该锁

0x9: 条件变量(condition variable)

作为一种同步手段,条件变量的作用类似于一个栅栏。对于条件变量,线程可以有2种操作

1. 线程可以等待条件变量,一个条件变量可以被多个线程等待
2. 线程可以唤醒条件变量,此时某个或所有等待此条件变量的线程都会被唤醒并继续运行

也就是说,使用条件变量可以让许多线程一起等待某个事件的发生,当事件发生时(条件变量被唤醒),所有的线程可以一起恢复执行

 

4. Ring3和Ring0的通信机制

开发和维护内核是一件很繁杂的工作,因此,只有那些最重要或者与系统性能息息相关的代码才将其安排在内核中。其它程序,比如GUI,管理以及控制部分的代码,一般都会作为用户态程序。在linux系统中,把系统的某个特性分割成在内核中和在用户空间中分别实现一部分的做法是很常见的。现在,越来越多的应用程序需要编写内核级和用户级的程序来一起完成具体的任务,通常采用以下模式

1. 编写内核服务程序利用内核空间提供的权限和服务来接收、处理和缓存数据
2. 编写用户程序来和先前完成的内核服务程序交互
    1) 利用用户程序来配置内核服务程序的参数
    2) 提取内核服务程序提供的数据
    3) 向内核服务程序输入待处理数据

比较典型的应用包括

1. Netfilter(内核服务程序:防火墙)、Iptables(用户级程序: 规则设置程序)
2. IPSEC(内核服务程序: VPN协议部分)、IKE(用户级程序: vpn密钥协商处理)
3. 大量的设备驱动程序及相应的应用软件

这些应用都是由内核级和用户级程序通过相互交换信息来一起完成特定任务的 

用户程序和内核的信息交换是双向的

1. 用户空间程序向内核空间模块"发送"信息
2. 用户空间程序主动地从内核空间模块"提取"数据
3. 从内核空间模块向用户空间"推送"数据

按照信息交互按信息传输发起方可以分为两大类

1. 用户向内核传送/提取数据
2. 内核向用户空间提交请求 

0x1: Sharing Memory Between Drivers and Applications

在驱动编程中,我们常常需要在驱动和用户程序间共享内存。可以使用的两种技术方案是:

1. 使用IOCTL共享Buffer(应用程序分配共享内存)
使用一个IOCT描述的Buffer,在驱动和用户程序间共享内存是内存共享最简单的实现形式。使用IOCTL共享的Buffer方,驱动编写者需要注意的的是对于特定的IOCTL采取哪种Buffer method
    1) METHOD_XXX_DIRECT(ring3数据传给ring0驱动)
        1.1) 在METHOD_XXX_DIRECT模式下,IO管理器为应用层指定的输出缓冲区(OutputBuffer)创建一个MDL锁住该应用层的缓冲区内存
        1.2) 内核会检查用户Buffer将被检查是否正确存取,只有检查通过后用户Buffer才会被锁进内存
        1.3) 在内核层中使用MmGetSystemAddressForMdlSafe将用户Buffer映射到内核地址空间,并获得应用层输出缓冲区所对应的(mapping)内核层地址
        1.3)MDL地址被放在了Irp->MdlAddress中
    这种方式的一个优点就是驱动可以在任意进程上下文、任意IRQL优先级别上存取共享内存Buffer
    
    2) METHOD_NEITHER(从驱动返回数据给应用程序或者做双向数据交换)(METHOD_NEITHER不建议使用,还是使用直接IO好)  
    使用METHOD_NEITHER方式描述一个共享内存Buffer存在许多固有的限制和需要小心的地方。(基本上,在任何时候一个驱动使用这种方式都是一样的)。其中最主要的规则是驱动只能在发起请求进程的上下文中存取Buffer。这是
因为要通过Buffer的用户虚拟地址存取共享内存Buffer。这也就意味着驱动必须要在设备栈的顶端,被用户应用程序经由IO Manager直接调用。期间不能存在中间层驱动或者文件系统驱动在我们的驱动之上。在实际情况下,WDM驱动将
严格限制在其Dispatch例程中存储用户Buffer。而KMDF驱动则需要在EvtIoInCallerContext事件回调函数中使用。 另外一个重要的固有限制就是使用METHOD_NEITHER方式的驱动要存取用户Buffer必须在PASSIVE_LEVEL的IRQL级别。这是因为IO Manager没有把Buffer锁在内存中,因此驱动程序想要存取共享Buffer时,内存可能被换出去
了。如果驱动不能满足这个要求,就需要驱动创建一个mdl,然后将其共享Buffer锁进到内存中。
2. 驱动程序分配共享内存 这种方式是内核来分配内存空间 1) 使用MmAllocatePagesForMDL从主内存池中分配,返回得到一个MDL 2) 驱动为了使用该共享内存,采用MmGetSystemAddressForMdlSafe得到其内核地址 3) 内核调用MmMapLockedPagesSpecifyCache映射到应用层进程地址空间中 4) MmMapLockedPagesSpecifyCache函数返回用户层地址空间的起始地址,将其放在IOCTL中返回给用户应用程序 5) 在用户程序使用完这部分内存之后,内核调用MmFreePageFromMdl来释放内存页。并且调用IoFreeMdl来释放由MmAllocatePageForMdl(Ex)创建的MDL

code download:

http://files.cnblogs.com/LittleHann/Sharing_Memory_Between_Drivers_and_Applications.zip
http://www.osronline.com/article.cfm?article=39

0x2: Sharing Events Between Kernel-User Mode

事件机制可以和"ring3-ring0共享内存"配合使用,创建2个事件,一个用来同步内核对用户态的buffer读写、另一个用来同步用户态对内核buffer的读写

在Kernel-User的通信中使用事件通知机制的流程如下:

1. The user-mode app creates the event, and passes the handle to the event to the driver via an IOCTL;
2. The driver creates an event, and passes the handle to the event to the user mode app via an IOCTL;
3. The user-mode app creates the event with a pre-determined name, which the driver then opens;
4. The driver creates the event with a pre-determined name, which the user-mode app then opens.

code download

http://files.cnblogs.com/LittleHann/Sharing_Events_Between_Kernel-User_Mode.zip

0x3: Netlink技术: communication between kernel and user space with netlink(AF_NETLINK)

the mechanisms based on the socket interface. Sockets allow the Linux kernel to send notifications to a user space application. This is in contrast to the file system mechanisms, where the kernel can alter a file, but the user program does not know of any change until it choses to access that file. The socket based mechanisms allow the applications to listen on a socket, and the kernel can send them messages at any time. This leads to a communication mechanism in which user space and kernel space are equal partners.

netlink socekt是一种用于在内核态和用户态进程之间进行数据传输的特殊的IPC

1. 它通过为内核模块提供一组特殊的API
2. 为用户程序提供了一组标准的socket接口的方式

netlink的特点如下

1. netlink实现了一种全双工的通讯连接,类似于TCP/IP中使用AF_INET地址族一样,netlink socket使用地址族(socket_family)AF_NETLINK。每一个netlink socket在内核头文件"include/linux/netlink.h"中定义自己的协议类型
2. It is simple to interact with the standard Linux kernel as only a constant has to be added to the Linux kernel source code. There is no risk to pollute the kernel or to drive it in instability, since the socket can immediately be used.
3. Netlink sockets are asynchronous as they provide queues, meaning they do not disturb kernel scheduling. This is in contrast to system calls which have to be executed immediately.
4. Netlink sockets provide the possibility of multicast.
5. Netlink sockets provide a truly bidirectional communication channel: A message transfer can be initiated by either the kernel or the user space application.
6. They have less overhead (header and processing) compared to standard UDP sockets.(netlink比udp socket更高效)

netlink的函数声明如下

netlink_socket = socket(AF_NETLINK, socket_type, netlink_family);

参数说明

1. AF_NETLINK
netlink使用"AF_NETLINK"地址族

2. socket_type
netlink是一种面向数据包的服务
    1) SOCK_RAW
    2) SOCK_DGRAM 

3. netlink_family 
当前netlink客户端(连接发起方)需要连接的内核模块/netlink组
    1) NETLINK_ROUTE
        用户空间的路由守护程序之间的通讯通道,比如BGP,OSPF,RIP以及内核数据转发模块。用户态的路由守护程序通过此类型的协议来更新内核中的路由表

    2) NETLINK_W1
        Messages from 1-wire subsystem.

    3) NETLINK_USERSOCK
    接收用户态的socket数据包的协议
              Reserved for user-mode socket protocols.

    4) NETLINK_FIREWALL
    将内核态的netfilter的IPV4数据传送到用户态中所用的协议,被ip_queue这个内核模块使用 

    5) NETLINK_IP6_FW
    将内核态的netfilter的IPV6数据传送到用户态中所用的协议,被ip6_queue这个内核模块使用  
    
    6) NETLINK_NETFILTER
        Netfilter subsystem.

    7) NETLINK_INET_DIAG
        INET socket monitoring.

    8) NETLINK_NFLOG
    用户态的iptables管理工具和内核中的netfilter模块之间通讯的通道
    
    9) NETLINK_ARPD
    用来从用户空间管理内核中的ARP表。
       
    10) NETLINK_XFRM
        用于IPsec的通信协议

    11) NETLINK_SELINUX
        SELinux event notifications.

    12) NETLINK_ISCSI
        Open-iSCSI.

    13) NETLINK_AUDIT
        Auditing(审计目的)

    14) NETLINK_FIB_LOOKUP
        Access to FIB lookup from user space.

    15) NETLINK_CONNECTOR
        Kernel connector  
    
    16) NETLINK_DNRTMSG
        DECnet routing messages.

    17) NETLINK_KOBJECT_UEVENT
        Kernel messages to user space.

    18) NETLINK_GENERIC
        Generic netlink family for simplified netlink usage.

下面我们来一起看看如何使用netlink进行编程,以实现kernel和user mode的通信,我们将在代码中的注释中对netlink的api所涉及到的数据结构进行解释

对于netlink的内核编程值得注意的是,一个好的编程实践是采用异步的方式进行ring0和ring3的通信,因为在很多cpu密集型的服务器上常常会在短时间内产生大量的内核事件,这个情况下串联的同步的netlink可能会导致阻塞进行kernel crash,则异步技术是一个较好的解决方案

user_client.c(用户态程序)

#include <sys/stat.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <string.h>
#include <asm/types.h>
#include <linux/netlink.h>
#include <linux/socket.h>

#define NETLINK_TEST 17
#define MAX_PAYLOAD 1024  /* maximum payload size*/

/*
跟TCP/IP中的socket一样,netlink的bind()函数把一个本地socket地址(源socket地址)与一个打开的socket进行关联
netlink的"地址结构体"如下
struct sockaddr_nl
{
    sa_family_t    nl_family;   
    unsigned short nl_pad;     
    __u32          nl_pid;      
    __u32          nl_groups;   
} nladdr;
 */
struct sockaddr_nl src_addr, dest_addr;

/*
struct nlmsghdr 为 netlink socket 自己的消息头,这用于"多路复用"和"多路分解",netlink 定义的所有协议类型以及其它一些控制,netlink 的内核实现将利用这个消息头来多路复用和多路分解已经其它的一些控制,因此它也被称为netlink 控制块。因此,应用在发送 netlink 消息时必须提供该消息头
linux内核的netlink部分总是认为在每个netlink消息体中已经包含了下面的消息头,所以每个应用程序在发送netlink消息之前需要提供这个头信息:
struct nlmsghdr
{
    __u32 nlmsg_len;        //字段 nlmsg_len 指定消息的总长度,包括紧跟该结构的数据部分长度以及该结构的大小
    __u16 nlmsg_type;        //字段 nlmsg_type 用于应用内部定义消息的类型,它对 netlink 内核实现是透明的,因此大部分情况下设置为 0

    /*
    字段 nlmsg_flags 用于设置消息标志,可用的标志包括 
    #define NLM_F_REQUEST           1       //标志NLM_F_REQUEST用于表示消息是一个请求,所有应用首先发起的消息都应设置该标志
    #define NLM_F_MULTI             2       //标志NLM_F_MULTI 用于指示该消息是一个多部分消息的一部分,后续的消息可以通过宏NLMSG_NEXT来获得
    #define NLM_F_ACK               4       //宏NLM_F_ACK表示该消息是前一个请求消息的响应,顺序号与进程ID可以把请求与响应关联起来
    #define NLM_F_ECHO              8       //标志NLM_F_ECHO表示该消息是相关的一个包的回传
    /* Modifiers to GET request */
    #define NLM_F_ROOT      0x100   //标志NLM_F_ROOT 被许多 netlink 协议的各种数据获取操作使用,该标志指示被请求的数据表应当整体返回用户应用,而不是一个条目一个条目地返回。有该标志的请求通常导致响应消息设置 NLM_F_MULTI标志。注意,当设置了该标志时,请求是协议特定的,因此,需要在字段 nlmsg_type 中指定协议类型
    #define NLM_F_MATCH     0x200   //标志 NLM_F_MATCH 表示该协议特定的请求只需要一个数据子集,数据子集由指定的协议特定的过滤器来匹配
    #define NLM_F_ATOMIC    0x400   //标志 NLM_F_ATOMIC 指示请求返回的数据应当原子地收集,这预防数据在获取期间被修改       */
    #define NLM_F_DUMP      (NLM_F_ROOT|NLM_F_MATCH)    //标志 NLM_F_DUMP 未实现
    /* Modifiers to NEW request */
    #define NLM_F_REPLACE   0x100   //标志 NLM_F_REPLACE 用于取代在数据表中的现有条目
    #define NLM_F_EXCL      0x200   //标志 NLM_F_EXCL_ 用于和 CREATE 和 APPEND 配合使用,如果条目已经存在,将失败
    #define NLM_F_CREATE    0x400   //标志 NLM_F_CREATE 指示应当在指定的表中创建一个条目
    #define NLM_F_APPEND    0x800   //标志 NLM_F_APPEND 指示在表末尾添加新的条目

    内核需要读取和修改这些标志,对于一般的使用,用户把它设置为 0 就可以,只是一些高级应用(如 netfilter 和路由 daemon 需要它进行一些复杂的操作)
    */
    __u16 nlmsg_flags;    

    __u32 nlmsg_seq;        //Sequence number
    __u32 nlmsg_pid;        //Sending process 
};
*/
struct nlmsghdr *nlh = NULL;

/*
结构 struct iovec 用于把多个消息通过一次系统调用来发送
#include <sys/uio.h>
struct iovec 
{
    ptr_t iov_base;    //Starting address 
    size_t iov_len;    //Length in bytes  
};
*/
struct iovec iov;
int sock_fd;

/*
struct msghdr 
{
    //套接口地址成员msg_name与msg_namelen
    void         *msg_name;
    socklen_t    msg_namelen;

    //I/O向量引用msg_iov与msg_iovlen
    struct iovec *msg_iov;
    size_t       msg_iovlen;

    //附属数据缓冲区成员msg_control与msg_controllen
    void         *msg_control;
    size_t       msg_controllen;

    //接收信息标记位msg_flags
    int          msg_flags;
};
*/
struct msghdr msg;

int main(int argc, char* argv[]) 
{ 
    //1. 使用socket()函数创建一个socket 
    sock_fd = socket(PF_NETLINK, SOCK_RAW, NETLINK_TEST);  
    
    //2. 初始化消息缓存区结构体 
    memset(&msg, 0, sizeof(msg));

    memset(&src_addr, 0, sizeof(src_addr));
    src_addr.nl_family = AF_NETLINK;
    /*
    sockaddr_nl的nl_pid属性的值可以设置为访问netlink socket的当前进程的PID,nl_pid作为这个netlink socket的本地地址 
    应用程序应该选择一个唯一的32位整数来填充nl_pid的值 
    */
    src_addr.nl_pid = getpid();  /* self pid */
    src_addr.nl_groups = 0;  /* not in mcast groups */
    //3. 跟TCP/IP中的socket一样,netlink的bind()函数把一个本地socket地址(源socket地址)与一个打开的socket进行关联 
    bind(sock_fd, (struct sockaddr*)&src_addr, sizeof(src_addr));
     
    /*
    4. 为了能够把一个netlink消息发送给内核或者别的用户进程,类似于UDP数据包发送的sendmsg()函数一样,我们需要另外一个结构体struct sockaddr_nl nladdr作为目的地址
        1) 如果这个netlink消息是发往内核的话,nl_pid属性和nl_groups属性都应该设置为0 
        2) 如果这个消息是发往另外一个进程的单点传输消息,nl_pid应该设置为接收者进程的PID,nl_groups应该设置为0 
    */
    memset(&dest_addr, 0, sizeof(dest_addr));
    dest_addr.nl_family = AF_NETLINK;
    dest_addr.nl_pid = 0;   /* For Linux Kernel */
    dest_addr.nl_groups = 0; /* unicast */

    /*
    5. netlink消息同样也需要它自身的消息头,这样做是为了给所有协议类型的netlink消息提供一个通用的背景。
    由于linux内核的netlink部分总是认为在每个netlink消息体中已经包含了下面的消息头,所以每个应用程序在发送netlink消息之前需要提供这个头信息:
    */
    nlh=(struct nlmsghdr *)malloc(NLMSG_SPACE(MAX_PAYLOAD));
    /* Fill the netlink message header */
    nlh->nlmsg_len = NLMSG_SPACE(MAX_PAYLOAD);
    nlh->nlmsg_pid = getpid();  /* self pid */
    nlh->nlmsg_flags = 0;
    /* Fill in the netlink message payload */
    strcpy(NLMSG_DATA(nlh), "Hello you!");
 
    //6. 一个netlink消息体由nlmsghdr消息头和消息的payload部分(由很多struct msghdr组成)组成。一旦输入一个消息,它就会进入一个被nlh指针指向的缓冲区 
    iov.iov_base = (void *)nlh;
    iov.iov_len = nlh->nlmsg_len;
    msg.msg_name = (void *)&dest_addr;
    msg.msg_namelen = sizeof(dest_addr);
    msg.msg_iov = &iov;
    msg.msg_iovlen = 1; 

    //7. 在完成了以上步骤后,调用一次sendmsg()函数就能把netlink消息发送出去: 
    sendmsg(sock_fd, &msg, 0);

    /* Read message from kernel 
    memset(nlh, 0, NLMSG_SPACE(MAX_PAYLOAD));
    recvmsg(sock_fd, &msg, 0);
    printf(" Received message payload: %s\n",
    NLMSG_DATA(nlh));
    */

    /* Close Netlink Socket */
    close(sock_fd);
}

kernel_server.c(内核态程序)

/*
内核空间的netlink API是由内核中的netlink核心代码支持的,在net/core/af_netlink.c中实现。
从内核的角度来说,API接口与用户空间的API是不一样的。内核模块通过这些API访问netlink socket并且与用户空间的程序进行通讯
*/
#include <linux/module.h>
#include <linux/init.h>
#include <linux/types.h>
#include <linux/sched.h>
#include <net/sock.h>
#include <linux/netlink.h>

#define NETLINK_TEST 17
struct sock *nl_sk = NULL;

/*
回调函数input()是在发送进程的系统调用sendmsg()的上下文被调用的。
如果input函数中处理消息很快的话,一切都没有问题。
但是如果处理netlink消息花费很长时间的话,我们则希望把消息的处理部分放在input()函数的外面,因为长时间的消息处理过程可能会阻止其它系统调用进入内核。取而代之,我们可以牺牲一个内核线程来完成后续的无限的的处理动
作。
*/ void input(struct sk_buff *skb) { struct nlmsghdr *nlh = NULL; unsigned char *payload = NULL; /*接收数据打印到内核消息*/ nlh = (struct nlmsghdr *)skb->data; payload = NLMSG_DATA(nlh); printk("%s\n",payload); } static int __init test_netlink(void) { printk("hi,netlink\n"); /* 1. 通过socket()调用来创建一个netlink socket */ nl_sk = netlink_kernel_create(&init_net,NETLINK_TEST,0,input,0,THIS_MODULE); return 0; } static void __exit exit(void) { sock_release(nl_sk->sk_socket); printk("bye,netlink\n"); } module_init(test_netlink); module_exit(exit);

Makefile

MODULE_NAME :=kernel_server
obj-m   :=$(MODULE_NAME).o
KERNELDIR ?= /lib/modules/$(shell uname -r)/build
PWD       := $(shell pwd)
all:
    $(MAKE) -C $(KERNELDIR) M=$(PWD)
    gcc -o user_client user_client.c
clean:
    rm -fr *.ko *.o *.cmd user_client

编译并载入内核

make
insmod kernel_server.ko

Relevant Link:

http://man7.org/linux/man-pages/man7/netlink.7.html
http://zh.wikipedia.org/zh/Netlink
http://bbs.chinaunix.net/thread-2029813-1-1.html
http://bbs.chinaunix.net/thread-4078272-1-1.html
http://www.cnblogs.com/hoys/archive/2011/04/09/2010788.html
http://blog.csdn.net/lovesunshine2008/article/details/4041755
http://www.osronline.com/article.cfm?article=39

值得注意的是,Netlink和TCP、UDP一样,也有端口port的概念,Netlink使用PID表示端口的概念: struct sock -> nl_pid

1. KERNEL Netlink
    1) nl_pid = 0
对于KERNEL态来说,在一个协议族下面只允许挂载一个nl_pid=0的Netlink

2. User Space Netlink
    1) nl_pid = rand()
    ..
    n) nl_pid = rand()
对于User Space态来说,在一个协议栈下面可以允许挂载多个nl_pid不同的Netlink

基于Netlink的这种性质,可以采用在内核态创建一个Netlink句柄,在用户态创建多个Netlink句柄,在内核态生产者速度远大于用户态消费者的场景下,实现一种多线程消费者的思想

0x4: 内核启动参数

Linux提供了一种通过 bootloader 向其传输启动参数的功能,内核开发者可以通过这种方式来向内核传输数据,从而控制内核启动行为

0x5: 模块参数、sysfs简单数据共享传输

内核子系统或设备驱动可以直接编译到内核,也可以编译成模块

1. 编译到内核: 可以通过启动参数方法通过内核启动参数来向它们传递参数
2. 编译成模块: 可以通过命令行在插入模块时传递参数,或者在运行时,通过sysfs来设置或读取模块数据

sysfs是一个基于内存的文件系统,实际上它基于ramfs,sysfs 提供了一种把内核数据结构、们的属性、以及属性与数据结构的联系开放给用户态的方式,它与kobject子系统紧密地结合在一起,因此内核开发者不需要直接使用它,而是内核的各个子系统使用它。用户要想使用sysfs读取和设置内核参数,仅需装载sysfs就可以通过文件操作应用来读取和设置,内核通过sysfs开放给用户的各个参数,对于sysfs其中的可配置文件(对应内核的某个或某类参数)来说,可以分为以下几类

1. a limit (e.g. maximum buffer size)
2. turn on or off a given functionality (for example routing)
3. represent some other kernel variable

Sysfs was designed to represent the whole device model as seen from the Linux kernel. It contains information about devices, drivers and buses and their interconnections. In order to represent the hierarchy and the interconnections sysfs is heavily structured and contains a lot of links between the individual directories. it contains the following 9 top-level directories

1. sys/block/:        all known block devices such as hda/ ram/ sda/
2. sys/bus/:        all registered buses. Each directory below bus/ holds by default two subdirectories:
3. device/:        for all devices attached to that bus
4. driver/:        for all drivers assigned with that bus.
5. sys/class/:        for each device type there is a subdirectory: for example /printer or /sound
6. sys/device/:        all devices known by the kernel, organised by the bus they are connected to
7. sys/firmware/:    files in this directory handle the firmware of some hardware devices
8. sys/fs/:        files to control a file system, currently used by FUSE, a user space file system implementation
9. sys/kernel/:        holds directories (mount points) for other filesystems such as debugfs, securityfs.
10. sys/module/:    each kernel module loaded is represented with a directory.
11. sys/power/:        files to handle the power state of some hardware

Linux/UNIX的一个特点就是把所有的东西都看作是文件(every thing is a file)。系统定义了简洁完善的驱动程序界面,客户程序可以用统一的方法透过这个界面和内核驱动程序交互
驱动程序运行于内核空间,用户空间的应用程序通过文件系统中/dev/目录下的一个文件来和它交互。这就是我们熟悉的那个文件操作流程:

open()
read()
write()
ioctl()
close()
/*
每个驱动程序按照自己的需要做独立实现,把自己提供的功能和服务隐藏在这个统一界面下。客户级程序选择需要的驱动程序或服务(其实就是选择/dev/目录下的文件),按照上述界面和文件操作流程,就可以跟内核中的驱动交互了。其实用面向对象的概念会更容易解释,系统定义了一个抽象的界面(abstract interface),每个具体的驱动程序都是这个界面的实现(implementation)

需要注意的是也不是所有的内核驱动程序都是这个界面,网络驱动程序和各种协议栈的使用就不大一致,比如说套接口编程虽然也有open()close()等概念,但它的内核实现以及外部使用方式都和普通驱动程序有很大差异
*/

For a kernel module there are three possibilities to use a file below:/sys

1. module parameter
2. register new subsystem
3. debugfs: debugfs, mounted in /sys/kernel/debug. 

sysfs_ex.c

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/fs.h>

//The first step is to declare our subsystem
static decl_subsys(myfs, NULL, NULL);

struct my_attr 
{
        struct attribute attr;
    int value;
};

static struct my_attr my_first = 
{
    .attr.name="first",
    .attr.owner = THIS_MODULE,
    .attr.mode = 0644,
    .value = 1,
};

static struct my_attr my_second = 
{
    .attr.name="second",
    .attr.owner = THIS_MODULE,
    .attr.mode = 0644,
    .value = 2,
};

/*
By design all attributes share the same show and store functions. Each time one of these two functions is invoked it gets the corresponding struct attribute as an argument. Therefore in the show and store functions you can obtain the value corresponding to the file being read/written and you can manipulate it accordingly
*/
static struct attribute * myattr[] = 
{
    &my_first.attr,
    &my_second.attr,
    NULL
};

static ssize_t default_show(struct kobject *kobj, struct attribute *attr,
                              char *buf)
{
        struct my_attr *a = container_of(attr, struct my_attr, attr);
    return scnprintf(buf, PAGE_SIZE, "%d\n", a->value);
}

static ssize_t default_store(struct kobject *kobj, struct attribute *attr,
                               const char *buf, size_t len)
{
        struct my_attr *a = container_of(attr, struct my_attr, attr);
           sscanf(buf, "%d", &a->value);
    return sizeof(int);
}

static struct sysfs_ops myops = {
    .show = default_show,
    .store = default_store,
};

static struct kobj_type mytype = {
    .sysfs_ops = &myops,
    .default_attrs = myattr,
};

static int __init sysfsexample_module_init(void)
{
    int err;
    //kobj_set_kset_s initializes myfs_subsys so that it will be part of the fs_subsys
    kobj_set_kset_s(&myfs_subsys, fs_subsys);
    //myfs_subsys.kobj.ktype points to a structure which holds all the attributes as well as the functions to read and write the attributes
    myfs_subsys.kobj.ktype = &mytype;
    //call to register_subsystem() registers our subsystem.
    err = subsystem_register(&myfs_subsys);

    return err;
}

static void __exit sysfsexample_module_exit(void)
{
    subsystem_unregister(&myfs_subsys);
}

module_init(sysfsexample_module_init);
module_exit(sysfsexample_module_exit);
MODULE_LICENSE("GPL");

sysfs_ex2.c

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/device.h>

/* the parameter value shows up in /sys/module/sysfs/parameters/ 
 * it can be read an changed, however it is not possible to do 
 * some "additional action" when it is read/written
 */
static int example_value;
module_param_named(value, example_value, int, S_IRUGO | S_IWUSR);
MODULE_PARM_DESC(value, "a test value");

/* a file called busval will show up under /sys/bus/my_pseudo_bus/
 * bus_show and bus_store is executed when it is read and written
 * respectively
 */
static int bus_value = 1;

static struct bus_type my_pseudo_bus = 
{
    .name = "my_pseudo_bus",
};


static ssize_t bus_show(struct bus_type *bus, char *buf) 
{
    return scnprintf(buf, PAGE_SIZE, "%d\n", bus_value);
}

static ssize_t bus_store(struct bus_type *bus, const char *buf, size_t count) 
{
    sscanf(buf, "%d", &bus_value);
        return sizeof(int);
}
BUS_ATTR(busval, S_IRUGO | S_IWUSR, bus_show, bus_store);

static int __init sysfsexample_module_init(void)
{
    int ret = -1;
    
    //First define bus my_pseudo_bus
    ret = bus_register(&my_pseudo_bus);
    if (ret < 0) 
    {
                 printk(KERN_WARNING "sysfs: error register bus: %d\n", ret);
        return ret;
         }
     
    //In the init function we register our pseudo bus and we create a file (attribute)
    ret = bus_create_file(&my_pseudo_bus, &bus_attr_busval);
    if (ret < 0) 
    {
        printk(KERN_WARNING "sysfs: error creating busfile\n");
        bus_unregister(&my_pseudo_bus);    
    }

    return 0;
}

static void __exit sysfsexample_module_exit(void)
{
    bus_remove_file(&my_pseudo_bus, &bus_attr_busval);
    bus_unregister(&my_pseudo_bus);
}

module_init(sysfsexample_module_init);
module_exit(sysfsexample_module_exit);
MODULE_LICENSE("GPL");

0x6: Debugfs

Debugfs is a simple to use RAM based file system especially designed for debugging purposes. Developers are encouraged to use debugfs instead of procfs in order to obtain some debugging information from their kernel code. Debugfs is quite flexible: it provides the possibility to set or get a single value with the help of just one line of code but the developer is also allowed to write its own read/write functions, and he can use the seq_file interface described in the procfs section.

Before having access to the debugfs it has to be mounted with the following command.
mount -t debugfs none /sys/kernel/debug

debugfs.c

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/debugfs.h>
#include <linux/uaccess.h>

u8 myvalue;
int file_value;
struct dentry *tmp, *dir, *file;
char mybuf[200];

static ssize_t my_read_file(struct file *file, char __user *userbuf,
                                size_t count, loff_t *ppos)
{
    return simple_read_from_buffer(userbuf, count, ppos, mybuf, 200);
} 

static ssize_t my_write_file(struct file *file, const char __user *buf,
                                size_t count, loff_t *ppos)
{
    if(count > 200)
        return -EINVAL;
    copy_from_user(mybuf, buf, count);
    return count;
}

static const struct file_operations my_fops = {
        .read = my_read_file,
        .write = my_write_file,
};
static int __init debugexample_module_init(void)
{
    /* the simplest interface */
    tmp = debugfs_create_u8("myfile", 0644, NULL, &myvalue);
    if (!tmp) {
        printk("error creating file");
        return -ENODEV;
    }

    /* custom read and write functions */
    dir = debugfs_create_dir("mydirectory", NULL);
    file = debugfs_create_file("myfile", 0644, dir, &file_value, &my_fops);

    return 0;
}

static void __exit debugexample_module_exit(void)
{
    debugfs_remove(tmp);
    debugfs_remove(file);
    debugfs_remove(dir);
}

module_init(debugexample_module_init);
module_exit(debugexample_module_exit);
MODULE_LICENSE("GPL");

0x7: sysctl

sysctl是一种用户应用来设置和获得运行时内核的配置参数的一种有效方式,通过这种方式,用户应用可以在内核运行的任何时刻来改变内核的配置参数,也可以在任何时候获得内核的配置参数,通常,内核的这些配置参数也出现在proc文件系统的/proc/sys目录下,用户应用可以直接通过这个目录下的文件来实现内核配置的读写操作

对于/proc虚拟文件系统下的子节点,都可以使用read、write对其进行操作,唯独/proc/sys不行,/proc/sys必须使用sysctl对其进行操作

The sysctl infrastructure is designed to configure kernel parameters at run time. The sysctl interface is heavily used by the Linux networking subsystem. It can be used to configure some core kernel parameters; represented as files in /proc/sys/*. The values can be accessed by using cat(1), echo(1) or the sysctl(8) commands. If a value is set by the echo command it only persists as long as the kernel is running, but gets lost as soon as the machine is rebooted. In order to change the values permanently they have to be written to the file /etc/sysctl.conf. Upon restarting the machine all values specified in this file are written to the corresponding files in /proc/sys/.

sysctl.c

#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/sysctl.h>

static struct ctl_table_header * test_sysctl_header;

int value1 = 0;
int value2 = 1;

int min = 10;
int max = 20;

/*
only values between min and max can be written to value1 and value2 respectively. 
Our directory has got two files: value1 and value2. Each of these files hold an integer variable which can have a value between 10 and 20. The user root is allowed to change the entries whereas normal user are allowed to read the entries.
*/
static ctl_table test_table[] = 
{
    {
        .ctl_name    = CTL_UNNUMBERED,
        .procname    = "value1",
        .data        = &value1,
        .maxlen        = sizeof(int),
        .mode        = 0644,
        .proc_handler    = &proc_dointvec_minmax,
        .strategy    = &sysctl_intvec,
        .extra1         = &min,
        .extra2         = &max
    },
    {
        .ctl_name    = CTL_UNNUMBERED,
        .procname    = "value2",
        .data        = &value2,
        .maxlen        = sizeof(int),
        .mode        = 0644,
        .proc_handler    = &proc_dointvec_minmax,
        .strategy    = &sysctl_intvec,
        .extra1         = &min,
        .extra2         = &max
    },
    { .ctl_name = 0 }
};

//New files and directories can be added by expanding one of the subtables. In this example we add a new directory called test below the /proc/sys/net/ directory
static ctl_table test_net_table[] = {
    {
        .ctl_name    = CTL_UNNUMBERED,
        .procname    = "test",
        .mode        = 0555,
        //Our directory has got two files: value1 and value2
        .child        = test_table
    },
    { .ctl_name = 0 }
};

static ctl_table test_root_table[] = 
{
    {
        .ctl_name    = CTL_UNNUMBERED,
        .procname    = "net",
        .mode        = 0555,
        //New files and directories can be added by expanding one of the subtables. In this example we add a new directory called test below the /proc/sys/net/ directory
        .child        = test_net_table
    },
    { .ctl_name = 0 }
};




static int __init sysctl_module_init(void)
{
    //Each entry in the /proc/sys directory is represented by an entry in a table maintained by the Linux kernel
    test_sysctl_header = register_sysctl_table(test_root_table);
    return 0;
}

static void __exit sysctl_module_exit(void)
{
    unregister_sysctl_table(test_sysctl_header);

}  

module_init(sysctl_module_init);
module_exit(sysctl_module_exit);
MODULE_LICENSE("GPL");

0x8: ioctl简单数据共享传输

The ioctl mechanism is implemented as a single system call which multiplexes the different commands to the appropriate kernel space function
有些内核开发者认为利用ioctl()系统调用往往会似的系统调用意义不明确,而且难控制。而将信息放入到proc文件系统中会使信息组织混乱,因此也不 赞成过多使用。他们建议实现一种孤立的虚拟文件系统来代替ioctl()和/proc,因为文件系统接口清楚,而且便于用户空间访问,同时利用虚拟文件系 统使得利用脚本执行系统管理任务更加方便、有效
There are different argument types for an ioctl.

1. The command does not require any data.
2. The command writes some data to the kernel.
3. The command reads some data from the kernel.
4. The kernel module reads the data and exchanges it with some new data.

ioctl_user.c

#include <stdio.h>
#include <sys/ioctl.h>
#include <fcntl.h>

#define MY_MACIG 'G'
#define READ_IOCTL _IOR(MY_MACIG, 0, int)
#define WRITE_IOCTL _IOW(MY_MACIG, 1, int)


int main()
{
    char buf[200];
    int fd = -1;
    if ((fd = open("/dev/cdev_example", O_RDWR)) < 0) 
    {
        perror("open");
        return -1;
    }
    //user space program that uses ioctl to send a message to the kernel
    if(ioctl(fd, WRITE_IOCTL, "hello world") < 0)
        perror("first ioctl");
    if(ioctl(fd, READ_IOCTL, buf) < 0)
        perror("second ioctl");

    printf("message: %s\n", buf);
    return 0;
}

ioctl.c

#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <asm/uaccess.h>

#define MY_MACIG 'G'
#define READ_IOCTL _IOR(MY_MACIG, 0, int)
#define WRITE_IOCTL _IOW(MY_MACIG, 1, int)
 
static int major; 
static char msg[200];

static ssize_t device_read(struct file *filp, char __user *buffer, size_t length, loff_t *offset)
{
      return simple_read_from_buffer(buffer, length, offset, msg, 200);
}


static ssize_t device_write(struct file *filp, const char __user *buff, size_t len, loff_t *off)
{
    if (len > 199)
        return -EINVAL;
    copy_from_user(msg, buff, len);
    msg[len] = '\0';
    return len;
}

char buf[200];
int device_ioctl(struct inode *inode, struct file *filep, unsigned int cmd, unsigned long arg)
{
    int len = 200;
    switch(cmd) 
    {
    case READ_IOCTL:    
        copy_to_user((char *)arg, buf, 200);
        break;
    
    case WRITE_IOCTL:
        copy_from_user(buf, (char *)arg, len);
        break;

    default:
        return -ENOTTY;
    }
    return len;

}

static struct file_operations fops = 
{
    .read = device_read, 
    .write = device_write,
    .ioctl = device_ioctl,
};

static int __init cdevexample_module_init(void)
{
    //Define your ioctl handler function in the struct file_operations (as was already done for the read and write handler function)
    major = register_chrdev(0, "my_device", &fops);
    if (major < 0) 
    {
             printk ("Registering the character device failed with %d\n", major);
             return major;
    }
    printk("cdev example: assigned major: %d\n", major);
    printk("create node with mknod /dev/cdev_example c %d 0\n", major);
     return 0;
}

static void __exit cdevexample_module_exit(void)
{
    unregister_chrdev(major, "my_device");
}  

module_init(cdevexample_module_init);
module_exit(cdevexample_module_exit);
MODULE_LICENSE("GPL");

0x9: procfs简单数据共享传输

proc是Linux提供的一种特殊的文件系统,推出它的目的就是提供一种便捷的用户和内核间的交互方式。它以文件系统作为使用界面,使应用程序可以以文件操作的方式安全、方便的获取系统当前运行的状态和其它一些内核数据信息。文件系统中的每一个文件对应于内核中的一个参数

1. proc文件系统多用于监视、管理和调试系统,很多管理工具如ps、top等,都是利用proc来读取内核信息的

2. 除了读取内核信息,proc文件系统还提供了写入功能。所以我们也就可以利用它来向内核输入信息。比如
    1) 通过修改proc文件系统下的系统参数配置文件(/proc/sys),我们可以直接在运行时动态更改内核参数 
    2) echo 1 > /proc/sys/net/ip_v4/ip_forward
    开启内核中控制IP转发的开关,我们就可以让运行中的Linux系统启用路由功能
//类似的,还有许多内核选项可以直接通过proc文件系统进行查询和调整 

3. 除了系统已经提供的文件条目,proc还为我们留有接口,允许我们在内核中创建新的条目从而与用户程序共享信息数据。比如
    1) 我们可以为系统调用日志程序(不管是作为驱动程序也好,还是作为单纯的内核模块也好)在proc文件系统中创建新的文件条目,在此条目中显示系统调用的使用次数,每个单独系统调用的使用频率等等,有些ROOTKIT就采用这种技术实现用户态和内核态的ROOTKIT的通信
    2) 我们也可以增加另外的条目,用于设置日志记录规则,比如说不记录open系统调用的使用情况等

4. provide information about the running system such as cpu information, information about interrupts, about the available memory or the version of the kernel.

5. information about "ide devices", "scsi devices" and "tty's".

6. networking information such as the arp table, network statistics or lists of used sockets

使用procfs需要注意的是

1. procfs是比较老的一种用户态与内核态的数据交换方式,内核的很多数据都是通过这种方式出口给用户的,内核的很多参数也是通过这种方式来让用户方便设置的。除了sysctl出口到/proc下的参数,procfs提供的大部分内核参数是只读的
2. procfs是基于Linux VFS文件系统架构的(everything is file),因为它也提供了类似于普通文件那样的read、write操作
3. procfs有一些限制,因为它提供的缓存,只有一个页,因此必须特别小心,并对超过页的部分做特别的考虑,处理起来比较复杂并且很容易出错,所有procfs并不适合于大数据量的输入输出 
4. procfs只适合于将KERNEL的参数导出(export)到USER SPACE(用户态),不适合于在KERNEL和USER SPACE之间传递大量的实时数据

procfs.c

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/proc_fs.h>
#include <asm/uaccess.h>

static struct proc_dir_entry *example_dir, *first_file;


static int write_first(struct file *file,
                             const char __user *buffer,
                             unsigned long count, 
                             void *data)
{
    char * kernel_buf;
    if (count < 0 || count > 1024)
        return -EFAULT;

    kernel_buf = kmalloc(count + 1, GFP_KERNEL);
        if(copy_from_user(kernel_buf, buffer, count)) {
        kfree(kernel_buf);
        return -EFAULT;
        }
    kernel_buf[count] = '\0';
    printk("write_first received data: %s\n", kernel_buf);
    printk("first filename %s\n", file->f_dentry->d_iname);
    printk("write_first data %s\n", (char *)data);

    return count; 
}

static int read_first(char *page, char **start,
                            off_t off, int count, 
                            int *eof, void *data)
{
    int offset = 0;
    char * message = "hello world: ";
    strcpy(page + offset, message);
    offset += strlen(message);
    memcpy(page + offset, data, strlen(data));
    offset += strlen(data);
    page[offset] = '\n';
    offset += 1;
    return offset;
}

static int __init procexample_module_init(void)
{
    /* create a directory */
    example_dir = proc_mkdir("example", NULL);
        if(example_dir == NULL)
                return -ENOMEM;
        example_dir->owner = THIS_MODULE;
        
        /* create a file */
        first_file = create_proc_entry("first", 0644, example_dir);
        if(first_file == NULL) {
        remove_proc_entry("example", NULL);
        return -ENOMEM;
    }
    first_file->data = kmalloc(strlen( "first file private data"), GFP_KERNEL);
    strcpy(first_file->data, "first file private data");
    first_file->read_proc = read_first;
    first_file->write_proc = write_first;
        first_file->owner = THIS_MODULE;
        return 0;
}

static void __exit procexample_module_exit(void)
{
    kfree(first_file->data);    
    remove_proc_entry("first", example_dir);
    remove_proc_entry("example", NULL);
}

module_init(procexample_module_init);
module_exit(procexample_module_exit);
MODULE_LICENSE("GPL");

Makefile

obj-m := procfs.o
KDIR := /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)

all:
    $(MAKE) -C $(KDIR) M=$(PWD) modules

clean:
    $(MAKE) -C $(KDIR) M=$(PWD) clean

0x10: Character Devices

As the name suggests, this interface was designed for character device drivers, and is commonly used for communication between uer and kernel space. (For example, users with sufficient privileges my write directly to the virtual terminal 1 with echo "hi there" > /dev/tty1).
Each module can register itself as a character device and provide some read and write functions which handle the data. Files representing character devices are located within the /dev directory (where you will also find block devices, but we will not be describing them further). Usually these files correspond to a hardware device.

cdev.c

#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <asm/uaccess.h>
 
static int major; 
static char msg[200];

static ssize_t device_read(struct file *filp, char __user *buffer, size_t length, loff_t *offset)
{
      return simple_read_from_buffer(buffer, length, offset, msg, 200);
}


static ssize_t device_write(struct file *filp, const char __user *buff, size_t len, loff_t *off)
{
    if (len > 199)
        return -EINVAL;
    copy_from_user(msg, buff, len);
    msg[len] = '\0';
    return len;
}

static struct file_operations fops = {
    .read = device_read, 
    .write = device_write,
};

static int __init cdevexample_module_init(void)
{
    major = register_chrdev(0, "my_device", &fops);
    if (major < 0) {
             printk ("Registering the character device failed with %d\n", major);
             return major;
    }
    printk("cdev example: assigned major: %d\n", major);
    printk("create node with mknod /dev/cdev_example c %d 0\n", major);
     return 0;
}

static void __exit cdevexample_module_exit(void)
{
    unregister_chrdev(major, "my_device");
}  

module_init(cdevexample_module_init);
module_exit(cdevexample_module_exit);
MODULE_LICENSE("GPL");

In contrast to most file system based approaches seen so far, the user has to create the device file explicitly with a call to:
mknod /dev/arbitrary_name c majorNumber minorNumber

0x11: UDP Sockets

briefly describe UDP sockets, since their usage in user space is well known and they provide a lot of flexibility. With UDP sockets it is possible to have communication between a kernel module on one system and a user space application on an other machine.

udpUser.c

#include <stdio.h>
#include <arpa/inet.h>
#include <string.h>

#define BUFFSIZE 5096 

int main(int argc, char *argv[]) {
    int sendlen, receivelen;
    int received = 0;
    char buffer[BUFFSIZE];
    struct sockaddr_in receivesocket;
    struct sockaddr_in sendsocket;
    int sock;
    
    int ret = 0;

    /* Create the UDP socket */
    if ((sock = socket(PF_INET, SOCK_DGRAM, IPPROTO_UDP)) < 0) {
        perror("socket");
        return -1;
    }

    /* my address */
    memset(&receivesocket, 0, sizeof(receivesocket));  
    receivesocket.sin_family = AF_INET; 
    receivesocket.sin_addr.s_addr = htonl(INADDR_ANY);
    receivesocket.sin_port = htons(9999);

    receivelen = sizeof(receivesocket);
    if (bind(sock, (struct sockaddr *) &receivesocket, receivelen) < 0) {
        perror("bind");
        return -1;        
    }

    /* kernel address */
    memset(&sendsocket, 0, sizeof(sendsocket));
    sendsocket.sin_family = AF_INET;
    sendsocket.sin_addr.s_addr = inet_addr("127.0.0.1");
    sendsocket.sin_port = htons(5555);

    /* Send message to the server */
    memcpy(buffer, "hello world", strlen("hello world") + 1);
    sendlen = strlen(buffer) + 1;
    
    if (sendto(sock, buffer, sendlen, 0, (struct sockaddr *) &sendsocket, sizeof(sendsocket)) != sendlen) {
        perror("sendto");
        return -1;
    }

    memset(buffer, 0, BUFFSIZE);
    if ((received = recvfrom(sock, buffer, BUFFSIZE, 0, NULL, NULL)) < 0){
        perror("recvfrom");
        return -1;
    }
    
    printf("message received: %s\n", buffer);

    return 0;
}

udpRecvCallback.c

#include <linux/module.h>
#include <linux/init.h>
#include <linux/in.h>
#include <net/sock.h>
#include <linux/skbuff.h>
#include <linux/delay.h>
#include <linux/inet.h>

#define SERVER_PORT 5555
static struct socket *udpsocket=NULL;
static struct socket *clientsocket=NULL;

static DECLARE_COMPLETION( threadcomplete );
struct workqueue_struct *wq;

struct wq_wrapper
{
        struct work_struct worker;
    struct sock * sk;
};

struct wq_wrapper wq_data;

static void cb_data(struct sock *sk, int bytes)
{
    wq_data.sk = sk;
    queue_work(wq, &wq_data.worker);
}

void send_answer(struct work_struct *data)
{
    struct  wq_wrapper * foo = container_of(data, struct  wq_wrapper, worker);
    int len = 0;
    /* as long as there are messages in the receive queue of this socket*/
    while((len = skb_queue_len(&foo->sk->sk_receive_queue)) > 0)
    {
        struct sk_buff *skb = NULL;
        unsigned short * port;
        int len;
        struct msghdr msg;
        struct iovec iov;
        mm_segment_t oldfs;
        struct sockaddr_in to;

        /* receive packet */
        skb = skb_dequeue(&foo->sk->sk_receive_queue);
        printk("message len: %i message: %s\n", skb->len - 8, skb->data+8); /*8 for udp header*/

        /* generate answer message */
        memset(&to,0, sizeof(to));
        to.sin_family = AF_INET;
        to.sin_addr.s_addr = in_aton("127.0.0.1");  
        port = (unsigned short *)skb->data;
        to.sin_port = *port;
        memset(&msg,0,sizeof(msg));
        msg.msg_name = &to;
        msg.msg_namelen = sizeof(to);
        /* send the message back */
        iov.iov_base = skb->data+8;
        iov.iov_len  = skb->len-8;
        msg.msg_control = NULL;
        msg.msg_controllen = 0;
        msg.msg_iov = &iov;
        msg.msg_iovlen = 1;
        /* adjust memory boundaries */    
        oldfs = get_fs();
        set_fs(KERNEL_DS);
        len = sock_sendmsg(clientsocket, &msg, skb->len-8);
        set_fs(oldfs);
        /* free the initial skb */
        kfree_skb(skb);
    }
}

static int __init server_init( void )
{
    struct sockaddr_in server;
    int servererror;
    printk("INIT MODULE\n");
    /* 
    socket to receive data 
    Create a socket to receive UDP packets
    */
    if (sock_create(PF_INET, SOCK_DGRAM, IPPROTO_UDP, &udpsocket) < 0) 
    {
        printk( KERN_ERR "server: Error creating udpsocket.n" );
        return -EIO;
    }
    server.sin_family = AF_INET;
    server.sin_addr.s_addr = INADDR_ANY;
    server.sin_port = htons( (unsigned short)SERVER_PORT);
    servererror = udpsocket->ops->bind(udpsocket, (struct sockaddr *) &server, sizeof(server ));
    if (servererror) 
    {
        sock_release(udpsocket);
        return -EIO;
    }
    /*
    we specify a callback function, which is executed every time a packet is received on that socket. This callback function is executed in "interrupt context". This implies that only a restricted set of operations can be performed in that function, and it is not allowed to send any messages.
    */
    udpsocket->sk->sk_data_ready = cb_data;
    
    /* 
    create work queue 
    we define a work_queue with which we can delay the sending of the answer until we have left interrupt context.
    */    
    INIT_WORK(&wq_data.worker, send_answer);
    wq = create_singlethread_workqueue("myworkqueue"); 
    if (!wq){
        return -ENOMEM;
    }
    
    /* 
    socket to send data 
    create a socket to send the answer back to the application
    */
    if (sock_create(PF_INET, SOCK_DGRAM, IPPROTO_UDP, &clientsocket) < 0) 
    {
        printk( KERN_ERR "server: Error creating clientsocket.n" );
        return -EIO;
    }
    return 0;
}

static void __exit server_exit( void )
{
    if (udpsocket)
        sock_release(udpsocket);
    if (clientsocket)
        sock_release(clientsocket);

    if (wq) {
                flush_workqueue(wq);
                destroy_workqueue(wq);
    }
    printk("EXIT MODULE");
}

module_init(server_init);
module_exit(server_exit);
MODULE_LICENSE("GPL");

Each time the module receives a packet, the callback function cb_data() is executed. The only thing this callback function does is to submit the send_answer() function to the work_queue. If the kernel decides that there is no better work to do, it executes the send_answer function. The send_answer() function receives the packets, by dequeuing them from the sockets-receive-queue, with skb_dequeue. Since more than one packet can be received by the socket between two consecutive executions of send_answer(), this function may need to dequeue multiple messages. The number of messages in the socket queue can be obtained with a call to skb_queue_len(). After having dequeued the packet the message is printed to the system log and a message is send back to the application with the sock_sendmsg() function. Since this function assumes to be executed from user space, we have to adjust the boundaries for the allowed memory region with the help of the set_fs macro.

需要明白的是,udpsocket这种通信方式比Netlink的效率要低,因为网络socket要过协议栈,Netlink只是简单的找句柄,然后copy

0x12: Sending Signals from the Kernel to the User Space

This approach is somewhat different from the others, since only the kernel can send a signal to the user space, but not vice versa. Additionally, the amount of data to be sent is quite limited. There are two types of signal APIs in user space

1. "normal" signals: which do not have any data
2. "realtime" signals: which carry 32 bits of data.

The main difference between them is that real time signals are queued, whereas normal signals are not. This means that if more than one normal signal is sent to a process before it is able to process it, it receives this signal only once, whereas he receives all real time signals.
The user space process registers a signal handler function with the kernel. This adds the address of the signal handler function to the process descriptor. This function gets executed each time a certain signal is delivered.
The sending phase of a signal consists of two parts(信号能得到处理的机会)

1. Update the process descriptor with the new signal.
2. If this process is to be rescheduled, or if it returns from an interrupt, it first checks whether there is a signal pending. If yes, it executes first the signal handler and only then does it continue with the rest of the program.

signal_user.c

#include <signal.h>
#include <stdio.h>
#include <string.h>
#include <fcntl.h>

#define SIG_TEST 44 /* we define our own signal, hard coded since SIGRTMIN is different in user and in kernel space */ 

void receiveData(int n, siginfo_t *info, void *unused) 
{
    printf("received value %i\n", info->si_int);
}

int main ( int argc, char **argv )
{
    int configfd;
    char buf[10];
    /* 
    setup the signal handler for SIG_TEST 
     SA_SIGINFO -> we want the signal handler function with 3 arguments
     */
    struct sigaction sig;
    sig.sa_sigaction = receiveData;
    sig.sa_flags = SA_SIGINFO;
    sigaction(SIG_TEST, &sig, NULL);

    /* 
    kernel needs to know our pid to be able to send us a signal 
    we use debugfs for this 
    do not forget to mount the debugfs!
     */
    configfd = open("/sys/kernel/debug/signalconfpid", O_WRONLY);
    if(configfd < 0) 
    {
        perror("open");
        return -1;
    }
    sprintf(buf, "%i", getpid());
    if (write(configfd, buf, strlen(buf) + 1) < 0) 
    {
        perror("fwrite"); 
        return -1;
    }
    
    return 0;
}

signal_kernel.c

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <asm/siginfo.h>    //siginfo
#include <linux/rcupdate.h>    //rcu_read_lock
#include <linux/sched.h>    //find_task_by_pid_type
#include <linux/debugfs.h>
#include <linux/uaccess.h>


#define SIG_TEST 44    // we choose 44 as our signal number (real-time signals are in the range of 33 to 64)

struct dentry *file;

static ssize_t write_pid(struct file *file, const char __user *buf, size_t count, loff_t *ppos)
{
    char mybuf[10];
    int pid = 0;
    int ret;
    struct siginfo info;
    struct task_struct *t;
    /* read the value from user space */
    if(count > 10)
        return -EINVAL;
    copy_from_user(mybuf, buf, count);
    sscanf(mybuf, "%d", &pid);
    printk("pid = %d\n", pid);

    /* send the signal */
    memset(&info, 0, sizeof(struct siginfo));
    info.si_signo = SIG_TEST;
    info.si_code = SI_QUEUE;    // this is bit of a trickery: SI_QUEUE is normally used by sigqueue from user space,
                    // and kernel space should use SI_KERNEL. But if SI_KERNEL is used the real_time data 
                    // is not delivered to the user space signal handler function. 
    info.si_int = 1234;          //real time signals may have 32 bits of data.

    rcu_read_lock();
    t = find_task_by_pid_type(PIDTYPE_PID, pid);  //find the task_struct associated with this pid
    if(t == NULL)
    {
        printk("no such pid\n");
        rcu_read_unlock();
        return -ENODEV;
    }
    rcu_read_unlock();
    ret = send_sig_info(SIG_TEST, &info, t);    //send the signal
    if (ret < 0) 
    {
        printk("error sending signal\n");
        return ret;
    }
    return count;
}

static const struct file_operations my_fops = 
{
    .write = write_pid,
};

static int __init signalexample_module_init(void)
{
    /* 
    we need to know the pid of the user space process
     we use debugfs for this. As soon as a pid is written to this file, a signal is sent to that pid
     */
    /* only root can write to this file (no read) */
    file = debugfs_create_file("signalconfpid", 0200, NULL, NULL, &my_fops);
    return 0;
}
static void __exit signalexample_module_exit(void)
{
    debugfs_remove(file);

}

module_init(signalexample_module_init);
module_exit(signalexample_module_exit);
MODULE_LICENSE("GPL");

mount -t debugfs none /sys/kernel/debug
kernel创建的debug结点实现了write的handle,当用户态向这个结点写入数据的时候,触发注册的write_pid回调,进入kernel的信号发送逻辑

0x13: Upcall

The upcall functionality of the Linux kernel allows a kernel module to invoke a function in user space. It is possible to start a program in user space, and give it some command line arguments, as well as setting environment variables.

callee.c

#include <string.h>

int main(int argc, char *argv[])
{
    char command[10] = "beep -r ";
    if(argv[1] != NULL) 
    {    
        strncat(command, argv[1], 1);
    }
    else
        strncat(command, "1", 1);
    system(command);
    return 0;
}

usermodehelper.c

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/proc_fs.h>
#include <asm/uaccess.h>


static int __init usermodehelper_example_init(void)
{
    int ret = 0;
    char *argv[] = {"/bin/ls", "2", NULL };
    char *envp[] = {"HOME=/", "PATH=/sbin:/usr/sbin:/bin:/usr/bin", NULL };

    printk("usermodehelper: init\n");
    /* last parameter: 1 -> wait until execution has finished, 0 go ahead without waiting*/
    /* returns 0 if usermode process was started successfully, errorvalue otherwise*/
    /* no possiblity to get return value of usermode process*/
    ret = call_usermodehelper("/zhenghan/upcall/callee", argv, envp, UMH_WAIT_EXEC);
    if (ret != 0)
        printk("error in call to usermodehelper: %i\n", ret);
    else
        printk("everything all right\n");
        return 0;
}

static void __exit usermodehelper_example_exit(void)
{
    printk("usermodehelper: exit\n");
}

module_init(usermodehelper_example_init);
module_exit(usermodehelper_example_exit);
MODULE_LICENSE("GPL");

0x14: mmap Portable Operating System Interface for UNIX® (POSIX) 共享的内存机制(shmem)共享内存

Memory mapping is the only way to transfer data between user and kernel spaces that does not involve explicit copying, and is the fastest way to handle large amounts of data.
There is some difference between the conventional read(2) and write(2) functions and mmap.

1. While data is transfered with mmap no "control" messages are exchanged. This means that a user space process can put data into the memory, but that the kernel does not know that new data is available. 
2. The same holds for the opposite scenario: The kernel puts its data into the shared memory, but the user space process does not get a notification of this event. 
3. This characteristic implies that memory mapping has to be used with some other communication means that transfers control messages, or that the shared memory needs to be checked in regular intervals for new content. 
4. Similar to the read and write function calls mmap can be used with different file systems and with sockets.

mmap_user.c

//user space program that will share a memory area with the kernel module
#include <stdio.h>
#include <string.h>
#include <fcntl.h>
#include <sys/mman.h>

#define PAGE_SIZE 4096

int main ( int argc, char **argv )
{
    int configfd;
    configfd = open("/sys/kernel/debug/mmap_example", O_RDWR);
    if(configfd < 0) 
    {
        perror("open");
        return -1;
    }

    char * address = NULL;
    //use debugfs and attach the memory area to a file,This allows the user space process to access the shared memory area with the help of a file descriptor.
    address = mmap(NULL, PAGE_SIZE, PROT_READ|PROT_WRITE, MAP_SHARED, configfd, 0);
    if (address == MAP_FAILED) 
    {
        perror("mmap");
        return -1;
    }

    printf("initial message: %s\n", address);
    memcpy(address + 11 , "*user*", 6);
    printf("changed message: %s\n", address);
    close(configfd);    
    return 0;
}

kernel需要实现的是,基于和用户态共享的某个结点(设备dev、/proc结点、debugfs结点)实现相应的操作句柄(open、mmap、close..),将用户态传递的mmap内存也映射到内核态,这样,用户态和内核态就可以共享同一块内存进行操作

mmap_simple_kernel.c

//kernel module that provides the mmap system call based on debugfs which represent a mmap memory
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/debugfs.h>

#include <linux/mm.h>  /* mmap related stuff */

struct dentry  *file1;

struct mmap_info 
{
    char *data;    /* the data */
    int reference;       /* how many times it is mmapped */      
};


/* keep track of how many times it is mmapped */

void mmap_open(struct vm_area_struct *vma)
{
    struct mmap_info *info = (struct mmap_info *)vma->vm_private_data;
    info->reference++;
}

void mmap_close(struct vm_area_struct *vma)
{
    struct mmap_info *info = (struct mmap_info *)vma->vm_private_data;
    info->reference--;
}

/* nopage is called the first time a memory area is accessed which is not in memory,
 * it does the actual mapping between kernel and user space memory
 */
struct page *mmap_nopage(struct vm_area_struct *vma, unsigned long address, int *type)
{
    struct page *page;
    struct mmap_info *info;
    /* is the address valid? */
    if (address > vma->vm_end) 
    {
        printk("invalid address\n");
        return NOPAGE_SIGBUS;
    }
    /* the data is in vma->vm_private_data */
    info = (struct mmap_info *)vma->vm_private_data;
    if (!info->data) 
    {
        printk("no data\n");
        return NULL;    
    }

    /* get the page */
    page = virt_to_page(info->data);
    
    /* increment the reference count of this page */
    get_page(page);
    /* type is the page fault type */
    if (type)
        *type = VM_FAULT_MINOR;

    return page;
}

struct vm_operations_struct mmap_vm_ops = {
    .open =     mmap_open,
    .close =    mmap_close,
    .nopage =   mmap_nopage,
};

/*
This function initializes the vm_area_struct to point to the mmap functions specific to our implementation. mmap_open und mmap_close are used only for bookkeeping. mmap_nopages is called when the user space process references a memory area that is not in its memory. Therefore mmap_nopages does the real mapping between user space and kernel space. The most important function is virt_to_page which takes the memory area to be shared as an argument and returns a struct page * that can be used by the user space to access this memory area.
*/
int my_mmap(struct file *filp, struct vm_area_struct *vma)
{
    vma->vm_ops = &mmap_vm_ops;
    vma->vm_flags |= VM_RESERVED;
    /* assign the file private data to the vm private data */
    vma->vm_private_data = filp->private_data;
    mmap_open(vma);
    return 0;
}

//This function frees the memory allocated during my_open.
int my_close(struct inode *inode, struct file *filp)
{
    struct mmap_info *info = filp->private_data;
    /* obtain new memory */
    free_page((unsigned long)info->data);
        kfree(info);
    filp->private_data = NULL;
    return 0;
}

/*
In this function we allocate the memory that will later be shared with the user space process. Since memory mapping is done on a PAGE_SIZE basis we allocate one page of memory with get_zeroed_page(GFP_KERNEL) We initialize the memory with a message form the kernel that states the name of this file. We set the private_data pointer of this file to the allocated memory in order to access it later in the my_mmap and my_close function
*/
int my_open(struct inode *inode, struct file *filp)
{
    struct mmap_info *info = kmalloc(sizeof(struct mmap_info), GFP_KERNEL);
    /* obtain new memory */
        info->data = (char *)get_zeroed_page(GFP_KERNEL);
    memcpy(info->data, "hello from kernel this is file: ", 32);
    memcpy(info->data + 32, filp->f_dentry->d_name.name, strlen(filp->f_dentry->d_name.name));
    /* assign this info struct to the file */
    filp->private_data = info;
    return 0;
}

static const struct file_operations my_fops = 
{
    .open = my_open,
    .release = my_close,
    .mmap = my_mmap,
};

static int __init mmapexample_module_init(void)
{
    file1 = debugfs_create_file("mmap_example", 0644, NULL, NULL, &my_fops);
    return 0;
}

static void __exit mmapexample_module_exit(void)
{
    debugfs_remove(file1);

}

module_init(mmapexample_module_init);
module_exit(mmapexample_module_exit);
MODULE_LICENSE("GPL");

mmap共享内存的这种数据通信模式本质上是用户态(USER SPACE)向内核态(KERNEL SPACE)发起主动请求,包括

1. 申请共享(将同一个物理内存分别在用户态和内核态分别映射,用户态将虚拟映射的mmap地址传到内核,内核态将其转换为物理地址后,再次映射到内核的一个内存地址)
2. 写数据,向共享内存(基于某个结点: 设备、或者文件系统)中写入数据,触发内核注册的结点回调处理函数(例如.mmap、.open、.write..),内核完成处理后,将结果返回给用户态

在内核态,可以实现环形队列的形式,实现高速的无锁内存读写,同时,用户态也可以和内核态以信号作为同步方式,当内核态处理完数据之后,通知用户态进行处理

虚拟内存到物理内存的映射通过页表完成,这是在底层软件中实现的,硬件本身提供映射,但是内核管理表及其配置

这种技术方案的基本原理就是把同一块预留的"物理内存地址"映射到不同的"虚拟地址空间"(Linux的Memory映射机制允许物理内存和虚拟内存地址的1:N关系),使得这块物理内存对内核和对用户均可见。从而实现用户态与内核态之间的数据通信,同时扩大内核进程可以使用到的内核空间
系统调用mmap通常用来将文件映射到内存,以加快该文件的读写速度。当用mmap操作一个设备文件时,可以将设备上的存储区域映射到进程的地址空间,从而以内存读写的方法直接控制设备。如果我们在内核模块里申请了一段内存区域,也可以利用此方法,将这个过程映射到用户空间,以实现内核空间与用户空间之间的共享内存

本文的方法适用于拥有大容量内存的Linux操作系统主机,假设我们要映射的物理内存段为0x20000000 ~ 0x30000000,1G 字节大小,并且这段内存在系统启动阶段已经做了预留,专门用来实现内核与用户之间的数据交互 

1. 物理内存映射到用户空间的实现方法

Linux提供的mmap系统调用,主要功能是实现设备或特殊文件在用户虚拟空间中的映射。函数原型如下

void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
/*
1. addr: 期望要映射到用户空间的虚拟地址
2. length: 要映射的长度
3. prot
4. flags
5. fd: 文件描述字,一般由open()返回

return value: 真实映射的用户空间地址
*/

当用户调用mmap()时候,内核会完成以下工作

1. 在进程虚拟空间查找一块VMA
2. 将这VMA进行映射,如果设备驱动或文件系统的file_operations定义mmap(),则调用它
3. 将这个VMA插入到进程的VMA链表中
4. 为了执行mmap,驱动程序只用为需要映射的的物理地址范围建立合适的页表,而Linux内核提供remap_pfn_range函数,可以完成页表的一次性建立

内核驱动模块代码实现

/*shmem_module.c*/
#define PHY_ADDRESS_START 0x20000000 /*物理内存的起始地址为0x20000000*/
#define SHMEM_MAJOR 221 /*该驱动程序使用的主设备号为221*/

/*设置数据结构struct file_operations,定义对字符设备进行操作的必要函数*/
struct file_operations shmem_fops = 
{
    .mmap = shmem_mmap
};

/*定义内存映射的操作函数,主要使用remap_pfn_range函数完成页表的建立*/
int shmem_mrp(struct file * fd , struct vm_area_struct * vma)
{
    unsigned long phy_addr = PHY_ADDRESS_START;
    if(remap_pfn_range(vma, vma->vm_start, ((phy_addr)>> PAGE_SHIFT), vma->end_vma->start, vma->vm_page_prot))
      return –EAGAIN;
}

/*在模块初始化的时候注册定义的伪字符设备并把它和上面的struct file_operations关联起来*/
static int shmem_module_init()
{
    register_chrdev(SHMEM_MAJOR, "shmem", &shmem_fops );
}

static init shmem_module_exit()
{
    unregister_chrdev(SHMEM_MAJOR, "shmem");
}

module_init(shmem_module_init);
module_exit(shmem_module_exit);

使用方法

1. 模块文件编译完成后生成KO文件。在系统中加载该模块
2. 用户态程序实现
    1) 首先需要在文件系统中新建一个设备节点,用设备号和驱动关联起来
    mknod /dev/shmem c 221 1
    2) 然后在用户程序中打开设备,并调用mmap()
    File* fd = fopen("/dev/shmem", "wr");
    unsigned long queue_start = mmap(0x600000, 0x10000000, PROT_READ | PROT_READ, MAP_SHARED, fd, 0)
    /*
    0x6000000: 为期望映射到虚拟地址
    queue_start: 为物理共享内存段映射到用户虚拟地址空间后的起始地址
    */
物理地址与用户虚拟地址间存在线性关系,可以通过基址+偏移量的方式访问整段共享内存区 

关于如何在用户进程中预留映射虚拟地址,需要注意的是

1. mmap中的addr参数本身就不是真正内核映射给用户的虚拟地址(addr只是调用方期望映射到的虚拟地址),真正实际映射到的虚拟地
址是mmap中返回值给出的
2. 如果在这之前地址空间已经被使用,将导致函数返回的地址和用户期望映射的地址不一致
/*
解决问题的一种思路是: 首先在Linux用户程序的链接脚本文件(ldscript)中的BSS 段中增加一块空间: 0x20000000 ~ 0x30000000
通过这样预留的方式保证这块空间不会被heap或者动态库占用
*/

2. 物理内存映射到内核空间的实现方法

高段物理内存(0x20000000 ~ 0x30000000)在Linux下可以看作外设I/O资源,可以通过动态映射的方法来访问,即直接通过内核提供的ioremap函数动态创建一段外设I/O内存资源到内核虚拟地址的映射表,从而可以在内核空间中访问这段I/O资源
ioremap是一种更直接的内存"分配"方式,使用时直接指定物理起始地址和需要分配内存的大小,然后将该段"物理地址"映射到内核地址空间。ioremap用到的物理地址空间都是事先确定的,并不是分配一段新的物理内存,ioremap多用于设备驱动,可以让CPU直接访问外部设备的IO空间。ioremap能映射的内存由原有的物理内存空间决定

\linux-2.6.32.63\include\asm-generic\io.h
static inline void __iomem *ioremap(phys_addr_t offset, unsigned long size)
{
    return (void __iomem*) (unsigned long)offset;
}
#define __ioremap(offset, size, flags)    ioremap(offset, size)
/*
1. offset: 要映射的起始物理地址
2. size: 要映射的空间的大小
3. flags: 要映射空间和权限有关的标志
*/

该函数返回映射后的内核虚拟地址(3G-4G)。接着便可以通过读写该返回的内核虚拟地址去访问这段高段内存资源,在linux2.6内核中,使用ioremap最多只能映射1G大小的物理内存,因此内核可以访问到的空间增大到1G,ioremap得到的内核虚拟地址和物理地址之间存在线性关系,内核驱动模块代码实现

/*a.c*/
unsigned long kernel_addr = ioremap(0x20000000, 0x10000000); 
/*
调用ioremap函数

kernel_addr: 为物理共享内存段映射到内核虚拟地址空间后的起始地址
内核虚拟地址和物理地址之间存在线性关系,可以通过基址+偏移量的方式访问整段共享内存区
*/

借鉴了《情景分析》中的方法实现将内核态申请的内存以设备文件的形式映射到用户态

内核代码部分

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/mm.h>
#include <linux/gfp.h>

MODULE_LICENSE("GPL");

//SHM_SIZE表示共享内存区域的大小,以页面数为单位
//1 PAGE
#define SHM_SIZE 1 

//设备文件的主设备号
int dev_major = 256;
//次设备号
int dev_minor = 0;
//shmem指向共享内存区域的指针,供内核程序在操作此共享内存时使用
char* shmem;
//shm_page指向共享内存中起始页面的page结构
struct page* shm_page;

int symboler_open(struct inode*, struct file*);
int symboler_release(struct inode*,struct file*);
ssize_t symboler_read(struct file*,char *,size_t, loff_t *);
ssize_t symboler_write(struct file*,const char*,size_t, loff_t *);
int symboler_mmap (struct file*, struct vm_area_struct *);
long symboler_ioctl(struct file*,unsigned int, unsigned long);

//创建设备驱动均需要的file_operations数据结构
struct file_operations symboler_fops =
{
    owner: THIS_MODULE,
    open: symboler_open,
    release: symboler_release,
    read: symboler_read,
    write: symboler_write,
    unlocked_ioctl: symboler_ioctl,
    mmap: symboler_mmap,
};

struct symboler_dev
{
    int     sym_var;
    struct   cdev    cdev;
};

//指针symboler_dev表示我们虚拟出的字符设备
struct symboler_dev     *symboler_dev;

//这些是file_operations中的打开、关闭与读写函数
int symboler_open(struct inode*inode, struct file*filp)
{
    printk("%s()is called.\n", __func__);
    return 0;
}

int symboler_release(struct inode*inode, struct file*filp)
{
    printk("%s()is called.\n", __func__);
    return 0;
}

ssize_t symboler_read(struct file*filp, char *buf, size_t len, loff_t *off)
{
    printk("%s()is called.\n", __func__);
    return 0;
}

ssize_t symboler_write(struct file*filp, const char*buf, size_t len, loff_t *off)
{
    printk("%s()is called.\n", __func__);
    return 0;
}

/*
这就是最关键的mmap操作: symboler_mmap。当用户空间使用系统调用mmap操作我们的设备文件时,最终会执行到symboler_mmap
*/
int symboler_mmap (struct file*filp, struct vm_area_struct *vma)
{
    printk("%s()is called.\n", __func__);
    /*
    remap_pfn_range为处于"vma->vm_start"与"vma->vm_start + vma->vm_end - vma->vm_start"之间的虚拟内存区域建立页表
    1. vma: 表示虚拟区域,在执行symboler_mmap时,代表虚拟地址区域的vma结构已由sys_mmap创建并初始化完毕,并且作为参数供symboler_mmap使用
    2. page_to_pfn(shm_page): 与物理内存对应的页帧号,代表的页将被映射到该区域内
    3. vma->vm_start: 重新映射时起始的用户虚拟地址
    4. vma->vm_end - vma->vm_start: 需要映射到用户空间的内存大小,以字节为单位,表示被映射区域的大小
    5. vma->vm_page_prot: "保护(protection)"属性 
    */
    if(remap_pfn_range(vma, vma->vm_start, page_to_pfn(shm_page), vma->vm_end -vma->vm_start, vma->vm_page_prot))
        return -EAGAIN;

    vma->vm_ops =& symboler_remap_vm_ops;
    symboler_vma_open(vma);

    return 0;
}


//实现了vma的操作方法集合。vma的所有操作都定义在数据结构vm_operations_struct中 
void symboler_vma_open(struct vm_area_struct *vma)
{
    printk("%s()is called.\n", __func__);
}

void symboler_vma_close(struct vm_area_struct *vma)
{
    printk("%s()is called.\n", __func__);
}

static struct vm_operations_struct symboler_remap_vm_ops = 
{
    .open =symboler_vma_open,
    .close =symboler_vma_close,
};
 

int symboler_init(void)
{
    int ret, err;
    
    //宏MKDEV将主设备号与次设备号组合成一个32位整数
    dev_tdevno = MKDEV(dev_major, dev_minor);
    //函数register_chrdev_region将我们的字符设备注册到系统中
    ret = register_chrdev_region(devno,1, "symboler");
    
    if(ret < 0)
    {
        printk("symboler register failure.\n");
        return ret;
    }
    else
        printk("symboler register successfully.\n");

    symboler_dev = kmalloc(sizeof(struct symboler_dev), GFP_KERNEL);
    if(!symboler_dev)
    {
        ret = -ENOMEM;
        printk("create device failed.\n");
    }
    else
    {
        //初始化字符设备
        symboler_dev->sym_var = 0;
        cdev_init(&symboler_dev->cdev, &symboler_fops);
        symboler_dev->cdev.owner= THIS_MODULE;
        err = cdev_add(&symboler_dev->cdev,devno, 1);
        
        if(err < 0)
            printk("Add device failure\n");
    }
    
    //alloc_pages申请到了我们需要的页面,并返回该区域第一个页面的page结构
    shm_page = alloc_pages(__GFP_WAIT, SHM_SIZE);
    //page_address()函数将page结构转换成内核中可以直接访问的线性地址
    shmem = page_address(shm_page);
    //Shmem此时便指向了我们刚申请的内存区域的起始地址。可以对其进行直接读写操作,例如将字符串"hello,mmap"写到了共享区域中 
    strcpy(shmem, "hello,mmap\n");

    return ret;
} 

以上就是内核代码部分,主要思想是

1. 建立一个模拟的字符设备
2. 在它的驱动程序中申请一块物理内存区域
3. 利用mmap将这段物理内存区域映射到进程的地址空间中
4. 利用page_address将其转换为内核空间中可以使用的线性地址

我们还需要在/dev下建立一个设备文件,执行如下命令即可
mknod  /dev/shm c  256  0
接下来学习如何在用户空间中获得共享内存区域的地址

#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/mman.h>

int main(void)
{
    intfd;
    char*mem_start;

    fd= open("/dev/shm", O_RDWR);
    
    if((mem_start = mmap(NULL,4096, PROT_READ|PROT_WRITE,MAP_SHARED, fd, 0)) == MAP_FAILED)
    {
        printf("mmap failed.\n");
        exit(0);
    }
    
    printf("mem:%s\n", mem_start);
    
    return 0;
}  

运行程序后,便可以输出我们在内核中写入共享内存的字符串"hello,mmap" 

Relevant Link:

http://qvb3d.iteye.com/blog/1645830
http://blog.csdn.net/arethe/article/details/6941112
http://www.ibm.com/developerworks/cn/linux/l-kernel-memory-access/
http://www.kerneltravel.net/jiaoliu/005.htm
https://www.google.com.hk/url?sa=t&rct=j&q=&esrc=s&source=web&cd=5&ved=0CDQQFjAE&url=http%3a%2f%2fwww%2epaper%2eedu%2ecn%2fdownload%2fdownPaper%2f200812-441&ei=_9YPVeDYF4Xp8AW4xYCgCw&usg=AFQjCNEds1n32wc1jeZocoMRTKCVMi9h4A&cad=rjt

example code

Drv.c

#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/mm.h>
#include <linux/cdev.h>
#include <linux/errno.h>
#include <linux/types.h>
#include <linux/interrupt.h>
#include <linux/delay.h>
#include <linux/errno.h>
#include <linux/sched.h>
#include <linux/delay.h>
#include <asm/system.h>
#include <asm/uaccess.h>
#include <asm/io.h>
#include <linux/version.h>
//#include <asm/semaphore.h>
//#include <asm/arch/irqs.h>
//#include <asm/hardware.h>
 
#define    DRIVE_MAJOR                165
#define    DRIVE_NAME                "Test drv"

typedef    struct
{
    dev_t            dev_num ;
    struct    cdev    cdev ;    
            
}code_dev ;

static code_dev    test_dev ;
unsigned char data_source;
unsigned char *testmap;
unsigned char *kmalloc_area;
unsigned long  msize;

static int    test_open(struct inode *inode, struct file *filp)
{
    return 0 ;
}
static int    test_close(struct inode *inode, struct file *filp)
{
    return 0 ;
}

static ssize_t test_write(struct file *filp, const char __user *buf, size_t count, loff_t *ppos)
{
    if(copy_from_user(&data_source, buf, sizeof(data_source)))
    {
        printk("write error!\n");
    }
    return(sizeof(data_source));
}

static ssize_t test_read(struct file *filp, char __user *buf, size_t count, loff_t *ppos)
{
    if(copy_to_user(buf, &data_source, sizeof(data_source)))
    {
        printk("read error!\n");
    } 
    return(sizeof(data_source));
}
static int test_mmap(struct file *file, struct vm_area_struct *vma)
{
    int ret;
    ret = remap_pfn_range(vma, vma->vm_start, virt_to_phys((void *)((unsigned long)kmalloc_area)) >> PAGE_SHIFT, vma->vm_end-vma->vm_start, PAGE_SHARED);
    if(ret!=0)
    {
        return -EAGAIN;
    } 
    return ret;
}  
static int test_ioctl(struct inode *inode, struct file *filp, unsigned int cmd, unsigned long arg)
{
    int result;
    int i;
    switch(cmd) 
    {    
        case 0:
            {    
                result = 0;
            }            
            break;
        case 1:
            {    
                result = 1;
            }    
            break;
        case 2:
            {
                for(i=0;i<20;i++)
                {
                    printk("i=%d  %c\n",i,*(testmap+i)); 
                }    
                result=2;
            }
            break;    
        default:
        return -ENOTTY;            
    }
    return(result);
}    
static struct file_operations test_fs = 
{
    
    .owner         = THIS_MODULE ,
    .open         = test_open ,
    .release     = test_close ,
    .read        = test_read ,
    .write        = test_write ,
    .mmap           = test_mmap,
    .ioctl        = test_ioctl
};

static int    __init test_init(void)
{
    unsigned int     ret ;
    unsigned char *virt_addr;
    memset(&test_dev , 0 ,sizeof(test_dev)) ;
    test_dev.dev_num = MKDEV(DRIVE_MAJOR , 0) ;
    ret = register_chrdev_region(test_dev.dev_num , 1 ,DRIVE_NAME) ;
    if(ret < 0)
    {
        return(ret) ;
    }

    cdev_init(&test_dev.cdev , &test_fs) ;
    test_dev.cdev.owner = THIS_MODULE ;
    test_dev.cdev.ops = &test_fs ;
    
    printk("\nInit drv \n") ;
        
    ret = cdev_add(&test_dev.cdev , test_dev.dev_num , 1) ;
    if(ret < 0)
    {
        printk("cdev add error !\n") ;
        return(ret) ;
    }

    testmap=kmalloc(4096,GFP_KERNEL);
    kmalloc_area=(int *)(((unsigned long)testmap +PAGE_SIZE-1)&PAGE_MASK);
    if(testmap==NULL)
    {
        printk("Kernel mem get pages error\n");
    } 
    for(virt_addr=(unsigned long)kmalloc_area;virt_addr<(unsigned long)kmalloc_area+4096;virt_addr+=PAGE_SIZE)
    {
        SetPageReserved(virt_to_page(virt_addr)); 
    }
    memset(testmap,'q',100); 
    printk("Test drv reg success !\n") ;
    return 0 ;
}
    

static void __exit test_exit(void)
{
    printk("Test drv  exit\n") ;
    cdev_del(&test_dev.cdev) ;
    unregister_chrdev_region(test_dev.dev_num , 1) ;
}
    
MODULE_LICENSE("GPL") ;
module_init(test_init) ;
module_exit(test_exit)    ;

make

Tdrv.c

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <string.h>
#include <malloc.h>

#define  max_num  4096 

int main(int argc,char *argv[])
{
    int fd;
    int ret;
    unsigned char *rwc,*rrc;
    char *map;
    unsigned char ** newmap;

    rwc = malloc(sizeof(unsigned char));
    rrc = malloc(sizeof(unsigned char));
    *rwc = 50;
    *rrc = 30;

    fd = open("/dev/drvio1", O_RDWR);

    if(fd < 0)
    {
        printf("open file error!\n");
        return -1;
    } 

    ret = write(fd, rwc,sizeof(rwc));
    ret = read(fd, rrc,sizeof(rrc));
    printf("rwc =%d\nrrc =%d\n",*rwc,*rrc);
    *rwc = 10;
    ret = write(fd,rwc,sizeof(rwc));
    ret = read(fd,rrc,sizeof(rrc));
    printf("rwc =%d\nrrc =%d\n",*rwc,*rrc);
    ioctl(fd,2,0);

    if((map = (char *)mmap(NULL,max_num,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0)) == MAP_FAILED)
    {
        printf("mmap error!\n"); 
    }
    memset(map,'c',max_num);
    strcpy(map, "Welcome");
    ioctl(fd,2,0);
    munmap(map,4096);

    map=NULL;
    close(fd);

    return 0;
}

Makefile

obj-m := Drv.o
PWD       := $(shell pwd)

all:
    make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules

clean:
    rm -rf *.o *~ core .*.cmd *.mod.c ./tmp_version *.ko modules.order  Module.symvers

clean_omit:
    rm -rf *.o *~ core .*.cmd *.mod.c ./tmp_version modules.order  Module.symvers

insmod Dri.ko

mknod /dev/drvio1 c 165 0 

gcc Tdrv.c -o Tdrv

./Tdrv

一个可行的技术方案是

1. 数据传输方面
    1) mmap共享内存
    2) 唤醒队列
使用内存映像进行Ring0-Ring3通信,在内存中保留了一块空间,将其配置成环形队列,再把这块内存空间映射到在用户空间运行的数据处理程序,实际上,内存影射方式通常也正是应用在那些内核和用户空间需要快速大量交互数据的情况下,特别是那些对实时性要求较强的应用

2. 数据共享同步方面
    1) 使用信号
    信号在内核里的用途主要集中在通知用户程序出现重大错误,强行杀死当前进程,这时内核通过发送SIGKILL信号通知进程终止,内核发送信号使用send_sign(pid,sig)例程,可以看到信号发送必须要事先知道进程序号(pid),所以要想从内核中通过发信号的方式异步通知用户进程执行某项任务,那么必须事先知道用户进程的进程号才可。而内核运行时搜索到特定进程的进程号是个费事的工作,可能要遍历整个进程控制块链表。所以用信号通知特定用户进程的方法很糟糕,一般在内核不会使用。内核中使用信号的情形只出现在通知"当前进程"(可以从current变量中方便获得pid)做某些通用操作,如终止操作等
    2) 使用轻量级的锁

0x15: SYS V Message Queues Between KERNEL AND USER SPACE

server.c

#include <linux/module.h> // init_module, cleanup_module //
#include <linux/kernel.h> // KERN_INFO //
#include <linux/types.h> // uint64_t //
#include <linux/kthread.h> // kthread_run, kthread_stop //
#include <linux/delay.h> // msleep_interruptible //
#include <linux/syscalls.h> // sys_msgget //
#include "msg_bmk.h"

// Exernal declarations //
extern long k_msgsnd( int msqid, struct msgbuf *msgp, size_t msgsz, int msgflg );
extern long k_msgrcv( int msqid, struct msgbuf *msgp, size_t msgsz, long msgtyp, int msgflg );

// Function prototypes //
void handle_message( void );
int message_ready( void );
int run_thread( void *data );
void send_kernel_timing( uint64_t cycles );
// Global variables //
struct task_struct *msg_task;
int msqid;

/**
* Reads the pentium time stamp counter register.
*
* @return The number of cycles that have elapsed since boot.
*/
__inline__ uint64_t bmk_rdtsc( void )
{
    uint64_t x;
    __asm__ volatile("rdtsc\n\t" : "=A" (x));
    return x;
}

/**
* Called each time a client wishes to benchmark.
*/
void handle_message( void )
{
    int i;
    int result;
    struct msgbuf buf;
    uint64_t kern_cycles;
    uint64_t start;
    uint64_t stop;
    uint64_t difference;
    //printk( KERN_INFO "SERVER : Waiting for message\n" );
    //msleep_interruptible( 1 );
    result = k_msgrcv( msqid, (struct msgbuf *)&buf, sizeof( buf ),
    USER_TYPE, 0 );
    if( result < 0 )
    {
        printk( KERN_INFO
        "SERVER : Unable to receive message (%d)\n", result );
        return;
    }
    //printk( KERN_INFO "SERVER : Received message: %s\n",
    // buf.message );
    kern_cycles = 0;
    for( i = 0; i < TRIALS; i++ )
    {
        buf.mtype = KERNEL_TYPE;
        strncpy( buf.message, "Thanks for the message Client", BUFSIZE );
        //printk( KERN_INFO "SERVER : Sending message: %s\n",
        // buf.message );
        start = bmk_rdtsc();
        result = k_msgsnd( msqid, (struct msgbuf *)&buf,
        sizeof( buf ), 0 );
        stop = bmk_rdtsc();
        difference = stop - start;
        //printk( KERN_INFO "SERVER : Number of cycles: %llu\n",
        // difference );
        kern_cycles = kern_cycles + difference;
        if( result < 0 )
        {
            printk( KERN_INFO
            "SERVER : Unable to send message (%d)\n", result );
            return;
        }
        //printk( KERN_INFO "SERVER : Waiting for message\n" );
        //msleep_interruptible( 1 );
        result = k_msgrcv( msqid, (struct msgbuf *)&buf,
        sizeof( buf ), USER_TYPE, 0 );
        if( result < 0 )
        {
            printk( KERN_INFO
            "SERVER : Unable to receive message (%d)\n", result );
            return;
        }
        //printk( KERN_INFO "SERVER : Received message: %s\n",
        // buf.message );
    }
    send_kernel_timing( kern_cycles );
}

/**
* Checks the message queue for messages (peeks, but does not remove).
*
* @return TRUE (1) if message is queued and ready, FALSE (0)
otherwise.
*/
int message_ready( void )
{
    int result;
    result = k_msgrcv( msqid, NULL, 0, USER_TYPE, IPC_NOWAIT );
    if( result == -E2BIG )
    {
        return TRUE;
    }
    if( result != -ENOMSG )
    {
        printk( KERN_INFO "Unable to peek at message queue: (%d)\n", result );
    }
    return FALSE;
}

/**
* The entry point of the kernel thread which is the message benchmark
* server.
*
* @param data Any parameters for the kernel thread.
* @return The kernel thread exit status.
*/
int run_thread( void *data )
{
    #ifdef HIGH_KERN_PRIORITY
    int result;
    struct sched_param sp;
    sp.sched_priority = 99;
    result = sched_setscheduler( current, SCHED_FIFO, &sp );
    if( result == -1 )
    {
        printk( KERN_INFO "Unable to worsen priority\n" );
    }
    printk( KERN_INFO "SERVER : Running with Real-Time Priority: %lu\n", current->rt_priority );
    #endif
    msqid = sys_msgget( KEY, 0666 | IPC_CREAT );
    if( msqid < 0 )
    {
        printk( KERN_INFO "SERVER : Unable to obtain msqid\n" );
        return -1;
    }
    while( !kthread_should_stop() )
    {
        if( message_ready() )
        {
            printk( KERN_INFO "SERVER : Message ready\n" );
            handle_message();
        }
        msleep_interruptible( 1000 );
    }
    return 0;
}

/**
* Pass raw integer timing results to user space where
* floating point operations are allowed.
*
* @param cycles The raw cycles.
*/
void send_kernel_timing( uint64_t cycles )
{
    struct timeinfobuf buf;
    int result;
    buf.mtype = KERNEL_TYPE;
    buf.cycles = cycles;
    result = k_msgsnd( msqid, (struct msgbuf *)&buf, sizeof( buf ), 0 );
    if( result < 0 )
    {
        printk( KERN_INFO "Unable to send message\n" );
        return;
    }
}

/**
* Entry point of module execution.
*
* @return The status of the module initialization.
*/
int init_module()
{
    printk( KERN_INFO "SERVER : Initializing msg_server\n" );
    msg_task = kthread_run( run_thread, NULL, "msg_server" );
    return 0;
}

/**
* Exit point of module execution.
*/
void cleanup_module()
{
    int result;
    printk( KERN_INFO "SERVER : Cleaning up msg_server\n" );
    result = kthread_stop( msg_task );
    if( result < 0 )
    {
        printk( KERN_INFO "SERVER : Unable to stop msg_task\n" );
    }
    result = sys_msgctl( msqid, IPC_RMID, NULL );
    if( result < 0 )
    {
        printk( KERN_INFO "SERVER : Unable to remove message queue from system\n" );
    }
}
MODULE_LICENSE( "GPL" );
MODULE_AUTHOR( "LittleHann" );
MODULE_DESCRIPTION( "Message queues benchmark server" );

clinet_user.c

#include <stdlib.h> // exit //
#include <sys/types.h> // key_t //
#include <sys/ipc.h> // IPC_CREAT, ftok //
#include <sys/shm.h> // shmget, ... //
#include <sys/sem.h> // semget, semop //
#include <stdio.h> // printf //
#include <string.h> // strcpy //
#include <stdint.h> // uint64_t //
#include "shm_bmk.h"

// Processor frequency (floating point) //
const long double PROCESSOR_MHZ = 2266.819;

/**
* Read the pentium time stamp counter register.
*
* @return The number of elapsed cycles since boot.
*/
__inline__ uint64_t bmk_rdtsc()
{
    uint64_t x;
    __asm__ volatile ("rdtsc\n\t" : "=A" (x));
    return x;
}

// Function prototypes //
long double benchmark( void *shm, int semid );
void *connect( int shmid );
void disconnect( void *shm );
int getSEM( void );
int getSHM( void );
long double handleKernelTiming( void *shm );

/**
* Send message to server and perform benchmark.
*
* @param shmid The shared memory handle.
* @return The number of cycles for a send.
*/
long double benchmark( void *shm, int semid )
{
    int i;
    char msg[BUFSIZE];
    uint64_t start;
    uint64_t stop;
    long double user_cycles;
    uint64_t difference;
    struct sembuf sb = {0, 0, 0};
    user_cycles = 0.0;
    sb.sem_op = -1; // Lock sem 0 //
    if( semop( semid, &sb, 1 ) == -1 )
    {
        perror( "semop" );
        exit( -1 );
    }
    strncpy( msg, "*Hello Server", BUFSIZE );
    //printf( "CLIENT : Sending message: %s\n", msg );
    start = bmk_rdtsc();
    memcpy( shm, msg, BUFSIZE );
    stop = bmk_rdtsc();
    difference = stop - start;
    printf( "CLIENT : Initial Start Up: %llu\n", difference );
    for( i = 0; i < TRIALS; i++ )
    {
        strncpy( msg, "*How is the weather?", BUFSIZE );
        //printf( "CLIENT : Sending message: %s\n", msg );
        start = bmk_rdtsc();
        memcpy( shm, msg, BUFSIZE );
        stop = bmk_rdtsc();
        difference = stop - start;
        //printf( "Number of cycles: %lld\n", difference );
        user_cycles = user_cycles + difference;
    }

    /**
    * Notice we have left an asterisk in first byte
    * of shared memory for kernel to poll for.
    */
    sb.sem_op = 1; // Free sem 0 //
    if( semop( semid, &sb, 1 ) )
    {
        perror( "semop" );
        exit( -1 );
    }
    user_cycles = user_cycles / (long double)TRIALS;
    return user_cycles;
}

void *connect( int shmid )
{
    void *shm = NULL;
    shm = shmat( shmid, NULL, 0 );
    if( !shm )
    {
        perror( "shmat" );
        exit( -1 );
    }
    return shm;
}

void disconnect( void *shm )
{
    int result;
    result = shmdt( shm );
    if( result < 0 )
    {
        perror( "shmdt" );
        exit( -1 );
    }
}

/**
* Connect to the semaphore and obtain the handle.
*
* @return The handle to the semaphore.
*/
int getSEM()
{
    int semid;
    semid = semget( KEY, 1, 0 );
    if( semid == -1 )
    {
        perror( "semget" );
        exit( -1 );
    }
    return semid;
}

/**
* Connect to the shared memory and obtain the handle.
*
* @return The handle to the shared memory.
*/
int getSHM()
{
    int shmid;
    shmid = shmget( KEY, BUFSIZE, 0666 );
    if( shmid == -1 )
    {
        perror( "shmget" );
        exit( -1 );
    }
    return shmid;
}

long double handleKernelTiming( void *shm )
{
    uint64_t kernel_cycles;
    while( strncmp( shm, "~", sizeof( char ) ) != 0 )
    {
        sleep( 1 );
    }
    kernel_cycles = 0;
    memcpy( &kernel_cycles, shm + 1, sizeof( uint64_t ) );
    return (long double)kernel_cycles / (long double)TRIALS;
}

/**
* The entry point of the application.
*
* @param argc The number of arguments.
* @param argv The arguments.
* @return The program exit status.
*/
int main( int argc, char *argv[] )
{
    int shmid;
    int semid;
    void *shm;
    long double user_cycles;
    long double user_usecs;
    long double kernel_cycles;
    long double kernel_usecs;
    shmid = getSHM();
    semid = getSEM();
    shm = connect( shmid );
    user_cycles = 0.0;
    user_cycles = benchmark( shm, semid );
    kernel_cycles = handleKernelTiming( shm );
    user_usecs = user_cycles / PROCESSOR_MHZ;
    kernel_usecs = kernel_cycles / PROCESSOR_MHZ;
    printf( "CLIENT : Shared memory benchmark\n" );
    printf( "CLIENT : Message size: %d bytes\n", BUFSIZE );
    printf( "CLIENT : Number of iterations: %d\n", TRIALS );
    printf( "CLIENT : User cycles: %llf\n", user_cycles );
    //printf( "CLIENT : User nanoseconds: %llf\n",
    // user_usecs * 1000.0 );
    printf( "CLIENT : Kernel cycles: %llf\n", kernel_cycles );
    //printf( "CLIENT : Kernel nanoseconds: %llf\n",
    // kernel_usecs * 1000.0 );
    disconnect( shm );
    return 0;
}

Relevant Link:

Fast User-Kernel Data Transfer.pdf
Lightweight KernelUser Communication for RealTime and Multimedia Applications.pdf
sharing memory between kernel and user space in linux.pdf
The Comparison of Communication Methods between User and Kernel Space in Embedded Linux.pdf

0x16: 内核的段描述符号突破Linux保护模式实现内核态用户态内存互写 

\linux-2.6.32.63\arch\x86\include\asm\segment.h

//KERNEL
#define GDT_ENTRY_KERNEL_CS 2
#define GDT_ENTRY_KERNEL_DS 3

#define __KERNEL_CS    (GDT_ENTRY_KERNEL_CS * 8)
#define __KERNEL_DS    (GDT_ENTRY_KERNEL_DS * 8)

//USER
#define GDT_ENTRY_DEFAULT_USER_DS 5
#define GDT_ENTRY_DEFAULT_USER_CS 6

#define __USER_DS     (GDT_ENTRY_DEFAULT_USER_DS* 8 + 3)
#define __USER_CS     (GDT_ENTRY_DEFAULT_USER_CS* 8 + 3)

运行内核代码的时候用内核的段描述符号就可以直接访问用户空间,但运行用户代码的时候用户段描述符不能访问内核空间,这是Linux保护模式的机制

\linux-2.6.32.63\arch\x86\include\asm\uaccess.h

#define KERNEL_DS    MAKE_MM_SEG(-1UL)
#define USER_DS     MAKE_MM_SEG(TASK_SIZE_MAX)

#define get_ds()    (KERNEL_DS)
#define get_fs()    (current_thread_info()->addr_limit)
#define set_fs(x)    (current_thread_info()->addr_limit = (x))

Linux内核的保护模式通过特权级这个机制来将用户态和内核态进行区分,实现保护模式,保护模式最大的作用是内存的读写,即禁止处于用户态模式的进程(本质是CPU特权级)
在用户代码调用系统函数的时候,程序进入了系统内核代码,描述符也已经切换到了内核的描述符,这时可以直接访问用户空间或者内核空间
Linux 实现不同特权级的内存读写保护是基于current->addr_limit实现的,Linux内核提供了一些可以实现用户空间和内核空间数据传输 的系统调用,其中包含了指针地址空间的检测逻辑,只有用户传递的参数访问的空间<=current->addr_limit,才可以继续运行

如果我们希望在用户态可以直接读写内核态的内存

//临时设置用户空间限制为内核空间的范围,调用完了过后恢复 
unsigned long old_fs_value = get_fs(); 

set_fs(get_ds()); /* after this we can access the user space data */ 
open(filename, O_CREAT O_RDWR o_EXCL, 0640); 
set_fs(old_fs_value); /* restore fs... */ 

这种技术会引入其他的安全风险

1. 没有形成统一的保护方式,对内核空间的保护不好。内核代码编程不注意的话就可能使得用户程序突破内核空间的保护,我们实现用户态内存空间拷贝的时候,一定要使用系统提供的COPY函数(copy_from_user、copy_to_user...),不要自己实现简单的拷贝
2. 内核代码对系统函数调用时设置用户空间限制不好,使用set_fs(get_ds())、或者直接修改current->addr_limit打开了内核保护模式的边界,很有可能被第三方黑客程序利用,从而导致系统的内核空间保护失效 

Relevant Link:

http://www.enet.com.cn/article/2004/0729/A20040729328800.shtml
http://blog.csdn.net/zqy2000zqy/article/details/1137918

 

5. 远程网络通信

Relevant Link:

Copyright (c) 2014 LittleHann All rights reserved

 

posted @ 2014-08-13 08:13  郑瀚Andrew  阅读(1533)  评论(0编辑  收藏  举报