zoukankan      html  css  js  c++  java
  • Linux基础守护进程、高级IO、进程间通信

    守护进程(Daemon)

    前言

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

    编程规则

    • 设置umask为0

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

    • 调用setuid创建新会话

    • 重新设置但前目录

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

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

      #include <stdio.h>
      #include <stdlib.h>
      #include <unistd.h>
      #include <fcntl.h>
      #include <sys/types.h>
      #include <syslog.h>
      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....
      ");
      
                  // 后台进程不退出
                  while(1)
                      sleep(1);
      
              }
          }
      }

    出错处理

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

    Snip20161009_33

    单例

    守护程序往往只有一个实例,而不允许多个,可以用文件锁来实现单例。

    惯例

    惯例是指大家都这么做,不这么做显得不专业的事情。

    • 单例文件路径在/var/run目录下,内容为该进程ID

    • 配置文件应该在/etc目录下

    • 守护的启动脚本通常放在/etc/init.d目录下

    高级IO

    前言

    在文件IO中,学习了如何通过read和write来实现文件的读写。在这一章讨论一些高级的IO方式。

    非阻塞IO

    IO通常是阻塞的,比如读鼠标文件,如果鼠标未产生数据,那么读操作会阻塞,一直到鼠标移动,才能返回。这种阻塞的IO简化了程序设计,但是导致性能下降。

    使用O_NONBLOCK标记打开文件,那么read行为就是非阻塞的了。如果read不到数据,read调用会返回-1,errno被标记为EAGAIN。

    如果open时没有带上O_NONBLOCK,那么可以通过fcntl设置这个模式。

    记录锁

    如果多个进程/线程同时写文件,那么使用O_APPEND,可以保证写操作是原子操作,但是O_APPEND只写到文件末尾。

    如果需要修改文件内容,则无法使用O_APPEND了,需要使用记录锁来锁定文件,保证写操作的原子性。

    #include "../h.h"
     
    int main()
    {
        int fd = open("a.txt", O_RDWR);
     
        // lock it
        struct flock l;
        l.l_type = F_WRLCK;
        l.l_whence = SEEK_SET;
        l.l_start = 0;
        l.l_len = 128;
     
        int ret = fcntl(fd, F_SETLKW, &l);
        if(ret == 0)
        {
            printf("lock success ");
        }
        else
        {
            printf("lock failure ");
        }
     
        getchar();
        l.l_type = F_UNLCK;
        fcntl(fd, F_SETLKW, &l);
     
    }

    9.4 IO多路转接

    如果一个进程,同时要响应多路IO数据,那么这个程序设计将会很麻烦。一般程序都是需要响应多路IO的,比如GUI程序都需要处理鼠标和键盘文件。

    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <sys/select.h>
    #include <sys/types.h>
    #include <fcntl.h>
    #include <errno.h>
    
    //void FD_CLR(int fd, fd_set *set);
    // 将fd从set中拿掉
    //
    //int  FD_ISSET(int fd, fd_set *set);
    //判断fd是否在集合中
    //
    //void FD_SET(int fd, fd_set *set);
    //将fd加入到集合中
    //
    //void FD_ZERO(fd_set *set);
    //将集合清空
    
    // int select(int nfds, fd_set *readfds, fd_set *writefds,
    //                   fd_set *exceptfds, struct timeval *timeout);
    // int nfds: 要求是集合中最大的文件描述符+1
    // fd_set* readfds: 想读取的文件描述符集合,这个参数既是输入,也是输出参数
    // fd_set* writefds: 想写的文件描述符集合,一般为NULL
    // fd_set* execptfds:出错,异常的文件描述符集合,一般为NULL
    // struct timeval* timeout: 因为select是阻塞的调用,这个参数表示超过这个时间,无论文件描述符是否有消息,都继续往下执行
    // 返回值:-1表示失败,0表示超时,而且没有任何的事件,大于0表示有事件的文件描述符的数量
    
    int main()
    {
        int fd_key;
        int fd_mice;
    
        fd_key = open("/dev/input/event1", O_RDONLY);
        fd_mice = open("/dev/input/mice", O_RDONLY);
        if(fd_key < 0 || fd_mice < 0)
        {
            perror("open key mice");
            return 0;
        }
    
        // fd_set 文件描述符集合类型
        fd_set set;
        FD_ZERO(&set);
        FD_SET(fd_key, &set);
        FD_SET(fd_mice, &set);
    
        // 此时set中有两个文件描述符,分别是鼠标和键盘
        int nfds = fd_key > fd_mice ? fd_key : fd_mice;
        nfds ++;
    
        struct timeval tv;
        tv.tv_sec = 1; //
        tv.tv_usec = 0; // 微秒  1/1000000 秒
        int ret;
    RESELECT:
        ret = select(nfds, &set, NULL, NULL,  &tv); // 阻塞一秒
    
        if(ret < 0)
        {
            if(errno == EINTR) // 被中断打断
            {
                // 补救
                goto RESELECT;
            }
            return 0;
        }
    
        if(ret == 0)
        {
            
        }
        
        if(ret > 0)
        {
            // 用户动了鼠标或者键盘,从而鼠标文件描述符或者键盘文件描述符可读
            if(FD_ISSET(fd_key, &set))
            {
                printf("keyboard message
    ");
                // 键盘有消息
            }
            if(FD_ISSET(fd_mice, &set))
            {
                printf("mice message
    ");
                // 鼠标有消息
            }
        }
    }
     

     

    9.4.1 select

    select的作用是,让内核监听一个fd集合,当集合中的fd有事件时,select会返回有消息的fd子集。

    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <sys/select.h>
    #include <sys/types.h>
    #include <fcntl.h>
    #include <errno.h>
    
    // fd_set最多能容纳1024个文件
    //
    // unsigned int data[32];   32x32 = 1024  
    
    
    int main()
    {
        int fd_key;
        int fd_mice;
    
        fd_key = open("/dev/input/event1", O_RDONLY);
        fd_mice = open("/dev/input/mice", O_RDONLY);
    
        int nfds = fd_key > fd_mice ? fd_key : fd_mice;
        nfds ++;
    
        // 文件描述符集合的拷贝
        fd_set set1;
        fd_set set2; // set1 --> set2
        memcpy(&set2, &set1, sizeof(set1));
    
        while(1)
        {
            fd_set set;
            FD_ZERO(&set);
            FD_SET(fd_key, &set);
            FD_SET(fd_mice, &set);
    
            struct timeval tv;
            tv.tv_sec = 1; //
            tv.tv_usec = 0; // 微秒  1/1000000 秒
    
            int ret = select(nfds, &set, NULL, NULL, &tv);
            if(ret < 0)
            {
                if(errno == EINTR)
                    continue;
                return 0;
            }
    
            if(ret > 0)
            {
                if(FD_ISSET(fd_key, &set))
                {
                    // 既然鼠标有消息,就应该把数据都读出
                    char buf[1024];
                    read(fd_key, buf, sizeof(buf));
                    printf("key event
    ");
                }
                if(FD_ISSET(fd_mice, &set))
                {
                    char buf[1024];
                    read(fd_mice, buf, sizeof(buf));
                    printf("mice event
    ");
                }
            }
        }
    }

    9.4.2 epoll

    epoll的作用和select差不多,但是操作接口完全不同。

     
    #include <stdlib.h>
    #include <stdio.h>
    #include <errno.h>
    #include <sys/epoll.h>
    #include <sys/types.h>
    #include <fcntl.h>
    
    // 通过epoll来实现多路io复用
    int main()
    {
        int fd_key = open("/dev/input/event1", O_RDONLY);
        int fd_mice = open("/dev/input/mice", O_RDONLY);
    
        if(fd_key < 0 || fd_mice < 0)
        {
            perror("open mice and keyboard");
            return -1;
        }
    
        // 创建epoll对象,创建epoll的参数已经废弃了,随便填
        int epollfd = epoll_create(512);
        if(epollfd < 0)
        {
            perror("epoll");
            return -1;
        }
    
        // 把鼠标和键盘的文件描述符,加入到epoll集合中
        struct epoll_event ev;
        ev.data.fd = fd_key; // 联合体,这个联合体用来保存和这个文件描述符相关的一些数据,用于将来通知时,寻找文件描述符
        ev.events = EPOLLIN | EPOLLONESHOT; // epoll要监听的事件,读或者写
        epoll_ctl(epollfd, EPOLL_CTL_ADD, fd_key, &ev);
    
        ev.data.fd = fd_mice;
        epoll_ctl(epollfd, EPOLL_CTL_ADD, fd_mice, &ev);
        // 调用epoll_ctl时,第四个参数被epoll_ctl拷贝走
        
    
        struct epoll_event ev_out[2];
    
        while(1)
        {
            int ret = epoll_wait(epollfd, ev_out, 2, 2000);
            if(ret < 0)
            {
                if(errno == EINTR)
                    continue;
                return -2;
            }
    
            if(ret > 0)
            {
                int i;
                for(i=0; i<ret; ++i)
                {
                    if(ev_out[i].data.fd  == fd_mice)
                    {
                        // 鼠标有消息
    //                    char buf[1024];
    //                    read(fd_mice, buf, sizeof(buf));
                        printf("mice
    ");
                    }
                    else if(ev_out[i].data.fd == fd_key)
                    {
    //                    char buf[1024];
    //                    read(fd_key, buf, sizeof(buf));
                        printf("key
    ");
                    }
                }
            }
        }
    }

     select和epoll的区别

    selectepoll
    出现早
    大规模文件描述符效率低 大规模文件描述符效率高
    小规模是select效率高  
    使用位域来表示描述符集合 使用红黑树来保存文件集合

    Snip20161023_1

    Snip20161023_2

    存储映射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 <stdio.h>
    #include <stdlib.h>
    #include <fcntl.h>
    #include <sys/types.h>
    
    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
    ");
                }
                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 <fcntl.h>
    #include <sys/types.h>
    #include <sys/file.h>
    #include <stdio.h>
    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 <fcntl.h>
    #include <sys/types.h>
    #include <sys/file.h>
    #include <stdio.h>
    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
    ");
            //   flock(fd, LOCK_EX); // 排他锁
    
            // 可以对文件进行读操作
            sleep(1);
    
            flock(fd, LOCK_UN); // 解锁
        }
        else
        {
            printf("can not get lock
    ");
        }
    
        close(fd);
    }
    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <fcntl.h>
    #include <sys/types.h>
    #include <sys/file.h>
    
    int main()
    {
        int fd = open("a.txt", O_RDWR);
        pid_t pid = getpid();
        printf("process id is %d
    ", (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
    ");
    
        sleep(10);
    
        // 解锁
        l.l_type = F_UNLCK; 
        fcntl(fd, F_SETLKW, &l);
    
        close(fd);
    }
    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <fcntl.h>
    #include <sys/types.h>
    #include <sys/file.h>
    
    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
    ", (int)l.l_pid);
    
    #if 0
        fcntl(fd, F_SETLKW, &l); // F_SETLKW:锁文件,如果锁不上(原因:别人上锁了),就等
    
        printf("get lock
    ");
    
        sleep(10);
    
        // 解锁
        l.l_type = F_UNLCK; 
        fcntl(fd, F_SETLKW, &l);
    #endif
        close(fd);
    }

    10.7 锁

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

    10.8 信号量

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

    Snip20161009_34

  • 相关阅读:
    Spark 1.1.0 安装测试 (分布式 Yarn-cluster模式)
    HBase Mac OSX 安装笔记
    hbase centOS生产环境配置笔记 (1 NameNode, 1 ResourceManager, 3 DataNode)
    ssh 配置自动登录
    CentOS 修改线程数限制等(limits.conf)
    Hadoop Mac OSX 安装笔记
    centos7安装及部署zabbix监控
    设置linux中Tab键的宽度(可永久设置)
    基于NFS共享存储实现KVM虚拟机动态迁移
    KVM虚拟化平台环境部署
  • 原文地址:https://www.cnblogs.com/w-x-me/p/6412667.html
Copyright © 2011-2022 走看看