Linux基础守护进程、高级IO、进程间通信


守护进程(Daemon)

前言

Linux常用于服务器,程序通常不运行在前台。运行于前台的进程和终端关联,一旦终端关闭,进程也随之退出。因为守护进程不和终端关联,因此它的标准输出和标准输入也无法工作,调试信息应该写入到普通文件中,以便将来进行错误定位和调试。而且守护进程通常以root权限运行。

编程规则

  • 设置umask为0

  • 调用fork,并让父进程退出

  • 调用setuid创建新会话

  • 重新设置但前目录

  • 关闭不需要的文件描述符

  • 重定向标准输入/标准输出/标准错误到/dev/null

    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    int main()
    {
        pid_t pid = fork();
        if(pid == 0)
        {
            pid = fork();
            if(pid == 0)
            {
                // daemon process
                umask(0);  // 设置掩码
                setsid(); // 让自己变成session leader
                chdir("/");  // 修改当前目录
                chroot("/");
    
                // 获取最大的已经打开的文件描述符
                int maxfd = 1024; // 演示
                // 把所有文件关闭
                int i;
                for(i=0; i<=maxfd; ++i)
                {
                    close(i);
                }
    
                // 重定向0、1、2文件到/dev/null
                open("/dev/null", O_RDONLY); // 标准输入
                open("/dev/null", O_WRONLY);  // 标准输出
                open("/dev/null", O_WRONLY);  // 标准错误
                
                // printf(""); // --> aaa.txt 效率低下
                //
                syslog(LOG_ERR|LOG_KERN, "haha, this is syslog....\n");
    
                // 后台进程不退出
                while(1)
                    sleep(1);
    
            }
        }
    }

出错处理

由于不能再使用标准输入和输出,因此需要调用以下函数来输出调试信息。

存储映射IO

10.1 前言

进程间通信(IPC)方式有许多种。包括匿名管道、命名管道、socketpair、信号、信号量、锁、文件锁、共享内存等等。

由于进程之间的虚拟地址无法相互访问,但是在实际的系统中,经常要涉及进程间的通信,所以在Unix的发展中,人们创造了多种进程间通信的方式,而这些通信方式,都被Linux继承了过来。

进程间通信的原理,是在进程外的公共区域申请内存,然后双方通过某种方式去访问公共区域内存。

按照分类,进程间通信涉及三个方面:

  • 小数据量通信(管道/socketpair)

  • 大数据量通信(共享内存)

  • 进程间同步(socketpair/管道/锁/文件锁/信号量)

10.2 匿名管道

用于有亲缘关系的进程间通信,匿名管道是单工通信方式。

 

Snip20161024_2

内核的buffer究竟有多大?一个内存页尺寸。实际在Ubuntu下测试是64K。当缓冲区满的时候,write是阻塞的。

read管道时,如果管道中没有数据,那么阻塞等待。
read管道时,如果此时write端已经关闭,而此时管道有数据,就读数据,如果没有数据,那么返回0表示文件末尾。

write管道时,如果此时所有的read端已经关闭,那么内核会产生一个SIGPIPE给进程,SIGPIPE的默认会导致进程退出,如果此时进程处理了SIGPIPE信号,那么write会返回-1,错误码是EPIPE。

10.2.1 创建

pipe函数 pipe函数产生两个文件描述符来表示管道两端(读和写)。  

10.2.2 读写

read: 1. 如果管道有数据,读数据 2. 如果管道没有数据    此时写端已经关闭,返回0      如果写端没有关闭,阻塞等待   write: 1. 如果管道有空间,写数据,写入的数据长度依赖管道的buffer剩余的空间。如果剩余空间>=写入长度,那么数据全部写入,如果剩余空间<写入长度,那么写入剩余空间长度,并且write立即返回,返回值为写入的长度。 2. 如果管道没有剩余空间,那么阻塞。 3. 如果write时,读端已经关闭,那么程序产生一个SIGPIPE信号,导致程序终止。如果程序有处理SIGPIPE信号,那么程序不会终止,此时write返回-1,错误码标记为EPIPE。

10.2.3 应用

ps axu | grep a.out

单工:只能单方向通信
半双工:可以两个方向通信,但是同一时刻只能有一个方向通信
全双工:可以同时双方通信

10.3 命名管道

命名管道也是单工通信,但是比匿名相比,它可以用于非亲缘关系的进程。

10.3.1 创建

mkfifo 创建管道文件

10.3.2 打开读端

open("管道文件名",O_RDONLY); 如果此时没有其他进程打开写端,那么该open阻塞  

10.3.3 打开写端

open("管道文件名", O_WRONLY);  
#include 
#include 
#include 
#include 

int main()
{
    // 打开文件时,添加非阻塞属性
    //int fd = open("/dev/input/mice", O_RDONLY | O_NONBLOCK);

    // 先打开文件,再通过fcntl设置O_NONBLOCK属性
    int fd = open("/dev/input/mice", O_RDONLY);

    int flags = fcntl(fd, F_GETFL);
    flags |= O_NONBLOCK;
    fcntl(fd, F_SETFL, flags);

    while(1)
    {
        char buf[1024];
        int ret = read(fd, buf, sizeof(buf));
        if(ret == -1) // 错误发生
        {
            if(errno == EAGAIN || errno == EWOULDBLOCK) // EAGAIN错误码表示:底层没有数据,应该继续再尝试读 EWOULDBLOCK
            {
                //鼠标并没有移动,底层并没有数据可以读,这种不算真的错误
                printf("mouse not move\n");
            }
            else // 真的有错误发生了
            {
                return -1;
            }
        }
    }
}

10.4 socketpair

socketpair和匿名管道类似,但是它是全双工的。

10.4.1 创建

int fd[2]; socketpair(AF_UNIX, SOCK_STREAM, 0, fd);

Snip20161024_3

10.5 mmap实现共享内存

unix提供了一些内存共享机制,但是还是习惯使用mmap进行内存共享。

man 7 shm_overview

10.5.1 有亲缘关系的进程之间mmap共享

有亲缘的关系的父子进程,可以使用匿名映射,直接将虚拟地址映射到内存。

Snip20161024_4

10.5.2 无亲缘关系的进程之间mmap共享

如果进程之间没有亲缘关系,那么就需要一个文件来进行内存共享。

但是如果使用了硬盘文件,那么效率相对底下。最好使用内存文件来映射,效率更加高。

10.5.3 使用shm_open打开共享内存文件

shm_open:创建内存文件,路径要求类似/somename,以/起头,然后文件名,中间不能带/

10.6 文件锁

#include 
#include 
#include 
#include 
int main()
{
    int fd = open("a.txt", O_RDWR);

//    flock(fd, LOCK_SH); // 共享
    flock(fd, LOCK_EX); // 排他锁
    
    // 可以对文件进行读操作
    sleep(10);

    flock(fd, LOCK_UN); // 解锁

    close(fd);
}
#include 
#include 
#include 
#include 
int main()
{
    int fd = open("a.txt", O_RDWR);

    //    flock(fd, LOCK_EX); // 排他锁
    int ret = flock(fd, LOCK_SH|LOCK_NB); // 共享锁
    if(ret == 0)
    {

        printf("get lock\n");
        //   flock(fd, LOCK_EX); // 排他锁

        // 可以对文件进行读操作
        sleep(1);

        flock(fd, LOCK_UN); // 解锁
    }
    else
    {
        printf("can not get lock\n");
    }

    close(fd);
}
#include 
#include 
#include 
#include 
#include 
#include 

int main()
{
    int fd = open("a.txt", O_RDWR);
    pid_t pid = getpid();
    printf("process id is %d\n", (int)pid);

    // 锁文件开始位置的4K内容
    struct flock l;
    l.l_type = F_WRLCK;
    l.l_whence = SEEK_SET;
    l.l_start = 0;
    l.l_len = 4096;
    fcntl(fd, F_SETLKW, &l); // F_SETLKW:锁文件,如果锁不上(原因:别人上锁了),就等

    printf("get lock\n");

    sleep(10);

    // 解锁
    l.l_type = F_UNLCK; 
    fcntl(fd, F_SETLKW, &l);

    close(fd);
}
#include 
#include 
#include 
#include 
#include 
#include 

int main()
{
    int fd = open("a.txt", O_RDWR);

    // 锁文件开始位置的4K内容
    struct flock l;
    l.l_type = F_WRLCK;
    l.l_whence = SEEK_SET;
    l.l_start = 1024;
    l.l_len = 4096;

    fcntl(fd, F_GETLK, &l);

    printf("pid = %d\n", (int)l.l_pid);

#if 0
    fcntl(fd, F_SETLKW, &l); // F_SETLKW:锁文件,如果锁不上(原因:别人上锁了),就等

    printf("get lock\n");

    sleep(10);

    // 解锁
    l.l_type = F_UNLCK; 
    fcntl(fd, F_SETLKW, &l);
#endif
    close(fd);
}

10.7 锁

pthread_mutex_init的锁,可以用于进程间同步,但是要求锁变量在共享内存中。

10.8 信号量

信号量用于计数,而不用考虑进程竞争问题。

Snip20161009_34