zoukankan      html  css  js  c++  java
  • UNIX环境C语言进程管理、进程间通信

    ******进程管理******
    一、基本概念
      1、进程与程序
        进程就是运行中的程序,一个正在运行的程序可能包含多个进程,进程在操作系统中负责执行特定的任务
        程序是存储在硬盘中的文件,它包含机器指令和数据,是一个静态的实体
        进程或任务它是处理活动状态的计算机程序
      2、进程的分类
        a、交互进程:用户可以输入数据、也能看到程序的反馈信息
        b、批处理进程:由系统命令各流程控制语句组成的可执行的脚本文件(Makefile)
        c、守护进程:一直活跃着的进程,一般在后台运行,由操作系统的开启脚本或超级用户加载(init)
      3、查看进程 ps
        ps:简单显示当前用户用控制终端的进程
        a:显示所有用户的进程
        -u:以详细的信息
        -x:包括无控制终端的进程
        -w:以更大的宽度显示
      4、进程的信息表
        用户、进程号、cpu占有率、内存占有率、虚拟内存占有率、物理内存使用量、状态、开始时间、运行时间
      5、进程的状态
        O 就绪态
        R 运行态
        S 可被唤醒的睡眠
        D 不可被唤醒的睡眠态
        T 暂停,收到了SIGSTOP信号
        X 死亡态
        Z 僵尸态
        < 高优先级
        N 低优先级
        l 多线程进程
      6、如果进程A开启进程B,那么进程A就是进程B的父进程,进程B就是进程A的子进程,进程A可能也有父进程,进程B可能也有子进程
      7、子进程结束时会向父进程发送SIGCHLD信号,父进程在收到这个信号后默认选择忽略,如果父进程没有及时回收(显示调用wait),子进程就处于僵尸态
      8、父进程死掉前,会把它的子进程托付给守护进程(init),然后再向他的父进程发送SIGCHLD信号
      9、操作系统会为每一个进程分配一个标识符,可以使用getpid获取,当进程结束后,属于它的标识符会延时重用

    二、getxxid
      getpid 进程标识符
      getppid 父进程标识符
      getuid 进程的实际用户用户id
      getgid 进程的实际用户组id
      geteuid 进程的有效用户id
      getegid 进程的有效用户组id

      设置进程的组id标识符和用户id标识符
      chmod u+s 可执行文件
      chmod g+s 可执行文件
      通过这种方式可以让可执行的拥有者获取比它自己更高的权限

    三、fork
      pid_t fork(void)
      功能:创建一个子进程
        返回值:一次调用两个返回值
      父进程的分支会返回子进程的进程号,父进程只能在创建子进程的时候获取子进程的id
      子进程返回0,子进程能随时获取父进程的id
      1、通过fork创建的子进程只能通过返回值来分辨父子进程然后进程相应的分支,处理相应的任务
      2、通过fork创建的子进程会拷贝父进程的全局段、静态数据段、堆、栈、IO流缓冲区、并且父子进程共享代码段、共享文件描述符、文件指针
      3、fork返回调用成功后,父子进程谁先返回不一定,可以通过sleep/usleep来确保父子进程谁先执行
      4、当系统中进程的总数超过系统的限制时,fork将调用失败
      5、练习:使用fork配合sleep实现出僵尸进程和孤儿进程 //zfork.c lfork.c
      6、在fork之前的代码只有父进程执行,在fork之后的代码,父进程都有可能执行

    四、vfork
      pid_t vfork(void);
      功能:用来创建子进程
        返回值与fork一致
      1、使用vfork创建子进程时,父进程会先暂停,等子进程完全创建成功之后,父进程再开始执行
      2、使用vfork要和exec函数配合才能创建子进程
      if(0 == vfork())
      {
        exec加载子进程
      }
      父进程
      3、使用vfork不会复制父进程的任何数据,而是通过exec函数加载另一个可执行文件,这种创建子进程的效率要比fork要高
      4、exec函数不会返回,子进程一定比父进程先执行

      int execl(const char *path, const char *arg, ...);
        path:可执行文件路径
        arg:给可执行文件的参数,类似于命令行参数,必须以NULL结尾,第一个必须是可以执行文件名
        execl("path","a.out",NULL);

      1、通过exec创建的子进程会替换掉父进程给的代码段,不拷贝父进程的栈、堆、全局、静态数据段,会用新的可执行文件替换掉他们
      2、exec只是加载一个可执行文件,并创建进程,不会产生新的进程号
      3、只有exec函数执行结束(无论成功还是失败),父进程才能继续执行

      int execlp(const char *file, const char *arg, ...);
        file:可执行文件的文件名,会从PATH环境变量指定的位置去找可执行文件
        参数于exec一致

      int execle(const char *path, const char *arg,..., char * const envp[]);
      path和arg与execl一致,但最后要提供环境变量表

      int execv(const char *path, char *const argv[]);
      path与execl一致,参数以指针数据的方式提供

      int execvp(const char *file, char *const argv[]);

      int execvpe(const char *file, char *const argv[],char *const envp[]);

    五、进程的正常退出
      1、从main中return stats,父进程会得到stats的低八位
      2、调用exit(stats)函数,父进程会得到stats的低八位,此函数没有返回值
      在进程退出前:
        a、调用atexit、onexit注册的函数
        b、冲刷并关闭打开的文件
        c、再调用_exit/_Exit函数
        stats:EXIT_SUCCESS/EXIT_FAILURE
      3、_exit(stats)/_Exit(stats)函数
        父进程会得到stats的低八位的数据,进程退出前会托孤,向父进程发送SIGCHLD信号,并且此函数不返回
      4、进程的最后一个线程结束

    六、进程的异常退出
      1、进程调用了abort函数,触发了中止信号
      2、进程收到了某些信号(退出、段错误、除0)
      3、最后一个线程收到取消请求,并且对取消请求做出的响应

    七、进程的回收wait/waitpid
      #include <sys/types.h>
      #include <sys/wait.h>
      pid_t wait(int *status);
      功能:回收子进程,只要是子进程它都回收
        status:返回进程的结束状态,以及低八位数据
      要使用系统通过的宏才能解析
      为NULL时说明不要子进程的结束状态
      此调用一次只能回收一个子进程,任何一个子进程结束它都会停止并回收子进程,如果想使用它回收所有的子进程必须要不停的调用直到函数返回-1(说明没有子进程了)
      如果在调用之前就有子进程处于僵尸状态,会立即返回并回收僵尸子进程

      pid_t waitpid(pid_t pid, int *status, int options);
      功能:要回收指定的子进程
      pid:要回收的子进程的id
        <-1 等待进程组id是pid的绝对值的进程
        -1 等待任意进程结束
        0 等待与父进程同组的子进程结束
        >0 等待指定的子进程结束
      options:如果指定的进程没有结束,是否阻塞

    ******信号处理******
    一、基本概念
    1、中断:中止(不是终止)当前程序正在执行的任务,转而执行其它的任务
    硬中断:由硬件设备触发的中断(手机的按键)
    软中断:由其他程序触发的中断(信号,Qt中的信号和槽)

    2、不可靠信号
    a、小于SIGRTMIN(34)的信号都是不可靠信号(它是建立在早期机制上的一种信号,由怕目标收不到信号会多次触发)
    b、这种信号不是实时的产生的,也不可以排队所以导致信号可能丢失
    c、在处理这种信号时可以选择默认的处理方式、也可以注册一个处理函数(在有些系统中出来函数结束后就恢复成默认的处理方式)

    3、可靠信号
    a、[SIGRTMIN,SIGRTMAX]范围内的信号是可靠信号
    b、可靠信号支持排队,不会丢失,实时产生
    c、进程与系统之间的通信都是不可靠信号(当系统察觉到进程触发一些错误时给进程发的都是不可靠信号),在工业控制邻域一般都使用实时信号

    4、信号的来源
    硬件:操作系统察觉到硬件工作异常,向正在使用该硬件的进程发送一个信号
    软件:通过kill、raise、alarm等函数或命令产生的信号
    键盘:
    Ctrl+c
    Ctrl+\
    Ctrl+z

    5、信号的处理方式
    1、忽略,不做任何处理
    2、终止进程
    3、捕获并处理:
    当信号发生前向操作系统注册一个信号处理函数,当信号发生后调用该函数处理信号
    4、终止+产生core:
    core dump 记录内存的使用情况并写在core文件中,在ubuntu系统中默认不产生core文件,需要通过命令(ulimit- t unlimited)设置
    core文件是一个二进制文件,需要相应的调试工具才能解析(gdb)
    gcc -g code.c ->a.out
    a.out 出现错误产生core文件
    gdb a.out core 显示出产生错误的代码

    二、signal
    #include <signal.h>

    typedef void (*sighandler_t)(int);

    sighandler_t signal(int signum, sighandler_t handler);
    功能:想内核注册一个信号处理函数
    signum:要处理的信号
    handler:
    函数指针:表示该信号捕获并处理
    SIG_IGN:告诉内核不要再向当前进程发送该信号,如果是SIGCHLD,则同时表示当前进程的子进程由init回收
    SIG_DEL:恢复默认处理方式
    返回值:在设置信号处理方式之前该信号的处理方式

    注意:在某些系统中,信号的处理函数只能处理一次,处理函数调用结束后会恢复默认的处理方式,如果想持久处理需要在处理函数即将结束时再注册一次
    SIGKILL、STGSTOP信号不能被忽略、捕获处理

    三、子进程的信号处理
    1、通过fork创建的子进程会继承父进程的信号处理方式
    2、通过vfork+exec函数创建子进程无法继承父进程的信号处理函数、但会继承父进程的信号忽略

    四、发送信号
    1、键盘:
    Ctrl+c
    Ctrl+\
    Ctrl+z
    2、错误:
    除0(SIGFPE(8)) 算术异常
    非法内存访问(SIGEGV(11)) 段错误
    硬件故障(SIGBUS(7)) 总线异常
    3、命令
    kill -信号 进程号
    killall -信号 命令 可以向多个进程批量的发送信号
    4、函数
    int kill(pid_t pid,int sig);
    功能:向指定的进程发送信号

    int rais(int sig);
    功能:向自己发送信号

    五、pause
    int pause(void);
    功能:使调用的进程进入睡眠状态,直到有信号终止该进程或信号被捕获

    1、当信号触发后,会先执行信号处理函数,pause再返回
    2、pause要不不返回(没有信号触发),要么返回-1(有信号产生并处理完毕)

    六、sleep
    #include <unistd.h>
    unsigned int sleep(unsigned int seconds);
    功能:使用调用的进程睡眠seconds秒

    1、当进程睡足seconds秒后会有信号产生再返回
    2、如果是由于信号产生中的睡眠,则sleep会返回剩余的秒数

    七、alarm
    #include <unistd.h>
    unsigned int alarm(unsigned int seconds);
    功能:使用调用的进程在seconds后收到SIGALRM信号

    1、SIGALRM默认的处理方式是终止进程
    2、如果在上次SIGALRM信号产生之前再次设置,会返回剩余的秒数
    3、如果设置的秒数为0,表示取消之前的设置

    八、信号集与信号屏蔽
    1、什么是信号集:信号的集合sigset_t,由128个二进制组成、每一个二进制代表一个信号
    #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);
    功能:测试信号集中是否有某个信号
    返回值:有返回1,没有返回0,失败返回-1

    2、信号屏蔽
    每一个进程都有一个信号掩码(signal mask),也叫信号屏蔽码,它是一个信号集,其中包含了需要屏蔽的信号
    #include <signal.h>
    int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
    功能:设置进程新的信号掩码(信号屏蔽码),获取旧的信号屏蔽码
    how:修改信号掩码的方式
    SIG_BLOCK:向当前信号掩码中添加信号
    SIG_UNBLOCK:从当前信号掩码中删除信号
    SIG_SETMASK:用新的信号集替旧的信号掩码
    newset:新添加、删除、替换的信号集,也可以为空,由how说了算
    oldset:获取旧的信号掩码
    当newset为空时,就是在备份信号掩码

    为什么要信号屏蔽:
    当进程执行一些敏感操作时不希望被打扰(原子操作),但又不希望信号丢失(忽略),此时需要屏蔽信号
    屏蔽信号的目的不是为了不接收信号,而是延迟接收,当处理完要做的事情后,应该把屏蔽的信号还原
    当信号屏蔽时发生的信号会记录一次,这个信号设置为未决状态,当信号屏蔽结束后,会再发送一次
    不可靠信号在信号屏蔽期间无论信号发送多少次,信号解除屏蔽后,只发送一次
    可靠信号在信号屏蔽期间发生的信号会排队记录,在信号接触屏蔽后逐个处理
    在执行信号处理函数时,会默认把当前处理的信号屏蔽掉,执行完成后再恢复

    int sigpending(sigset_t *set);
    功能:获取未决状态的信号
    可以在解除信号屏蔽前预先查找有哪些未决的信号

    九、信号处理
    #include <signal.h>
    int sigaction(int signum, const struct sigaction *act,struct sigaction *oldact);
    功能:可以忽略信号、设置或获取信号处理方式
    act:设置心的信号处理方式
    oldact:获取旧的信号处理方式

    struct sigaction
    {
    void (*sa_handler)(int); //信号处理函数指针
    void (*sa_sigaction)(int, siginfo_t *, void *); //信号处理函数指针 需要使用sigqueue发送信号
    sigset_t sa_mask; //信号屏蔽码,执行信号处理函数时被屏蔽的信号(在执行信号处理函数时,默认屏蔽正在处理的函数,也可以添加其它要屏蔽的信号,在信号处理函数结束后恢复成默认)
    int sa_flags;
    SA_NOCLDSTOP:忽略SIGCHLD
    SA_NODEFER/SA_NOMASK:处理时不屏蔽信号
    SA_RESETHAND:处理完信号后,恢复默认处理方式
    SA_RESTART:当信号处理函数中断的系统调用,则重启
    SA_SIGINFO:用sa_sigaction处理信号
    void (*sa_restorer)(void);//保留
    };

    int sigqueue(pid_t pid, int sig, const union sigval value);
    功能:向指定的进程发送信号,并附带一些数据

    十、计时器
    操作系统维护了三个计时器
    真实计时器:程序运行的真实时间
    虚拟计时器:记录程序在用户态耗费的时间
    实用计时器:记录程序在用户态和内核态耗费的时间
    真实 = 实用 + 进出的耗费 + 休眠

    使用计时器定时做一些事情,类似闹钟的功能
    #include <sys/time.h>
    int getitimer(int which, struct itimerval *curr_value);
    功能:获取之前设置的定时任务
    which:计时器的类型
    ITIMER_REAL 真实,信号:SIGALRM
    ITIMER_VIRTUAL 虚拟,信号:SIGVTALRM
    ITIMER_PROF 实用,信号:SIGPROF
    curr_value:
    struct timeval it_interval:时钟信号的间隔时间
    struct timeval it_value:第一次时钟信号产生的时间

    struct timeval
    {
    long tv_sec; /* seconds */
    long tv_usec; /* microseconds */
    };

    int setitimer(int which, const struct itimerval *new_value,struct itimerval *old_value);
    功能:设置、获取闹钟
    new_value:新设置的闹钟
    NULL说明只能用来获取旧闹钟
    old_value
    NULL说明只设置不获取

    ******进程间通信******
    一、基本概念
    进程间通信(IPC):进程之间交换数据的过程叫进程间通信
    进程间通信的方式
    简单的进程间通信:
    命令行:父进程通过exec函数创建子进程时可以附加一些数据
    环境变量:父进程通过exec函数创建子进程顺便传递一张环境变量表
    信号:父子进程之间可以根据进程号相互发送信号,进行简单通信
    文件:一个进程向文件中写入数据,另一个进程从文件中读取出来
    命令行、环境变量只能单向传递,信号太过于简单,文件通信不能实时

    二、管道
    传统的进程间通信方式:管道
    1、管道是一种古老的通信的方式(基本上不再使用)
    2、早期的管道是一种半双工,现在大多数是全双工
    3、有名管道(这种管道是以文件方式存在的),适合任意进程之间的通信
    创建管道文件:
    命令mkfifo
    函数mkfifo
    #include <sys/types.h>
    #include <sys/stat.h>
    int mkfifo(const char *pathname, mode_t mode);
    管道通信的编程模式:
    进程A 进程B
    创建管道mkfifo
    打开管道open 打开管道
    写/读数据 read/write 读/写数据
    关闭管道close 关闭管道
    删除unlink/remove
    4、无名管道:由内核帮助创建,只返回管道的文件描述符,看不到管道文件,但这种管道只能用在fork创建的父子进程之间
    #include <unistd.h>
    int pipe(int pipefd[2]);
    功能:返回两个打开的管道文件描述符
    pipefd[0] 用来读数据
    pipefd[1] 用来写数据

    三、XSI IPC进程间通信
    1、XSI通信是靠内核的IPC对象进行通信
    2、每一个IPC对象都有一个IPC标识符(类似文件描述符),IPC标识符它是一个非零整数
    3、IPC对象必须要先创建,创建后才能进程获取、设置、操作、删除、销毁
    4、创建IPC对象必须要提供一个键值(key_t),健值是创建、获取IPC对象的依据
    5、产生健值的方法:
    固定的字面值:1980014
    使用函数计算:健值 = ftok(项目路径,项目id)
    使用宏让操作系统随机分配:IPC_PRIVTE
    必须把获取到的IPC对象标识符记录下来,告诉其它进程
    6、XSI可以创建的IPC对象有:共享内存、消息队列、信号量

    四、共享内存
    1、由内核维护一块共享的内存区域,其它进程把自己的虚拟地址映射到这快内存,然后多个进程之间就可以共享这快内存了
    2、这种进程间通信的好处是不需要信息复制,是进程间通信最快的一种方式
    3、但这种通信方式会面临同步的问题,需要与其它通信方式配合,最合适的就是信号

    共享内存的编程模式:
    1、进程之间要约定一个键值
    进程A 进程B
    创建共享内存
    加载共享内存 加载共享内存
    卸载共享内存 卸载共享内存
    销毁共享内存

    #include <sys/ipc.h>
    #include <sys/shm.h>
    int shmget(key_t key, size_t size, int shmflg);
    功能:创建共享内存(会在内核中开辟一块内存),如果要创建的对象已经存在,可以出错,也可以获取
    size:共享的大小,尽量是4096的倍数
    shmflg:
    创建:IPC_CREAT | IPC_EXCL |0744
    返回值:IPC对象标识符(类似文件描述符)

    void *shmat(int shmid, const void *shmaddr, int shmflg);
    功能:加载共享内存(进程的虚拟地址与内核中的共享内存映射)
    shimid:shmget的返回值
    shmaddr:进程提供的虚拟地址,如果为NULL,操作系统会自动选择一块地址映射
    shmflg:
    SHM_RDONLY:限制内存的权限为只读
    SHM_REMAP:映射已经存的共享内存
    SHM_RND:当shmaddr为空时自动分配
    SHMLBA:shmaddr的值不能为空,否则出错
    0
    返回值:映射后的内存的虚拟地址
    int shmdt(const void *shmaddr);
    功能:卸载共享内存(进程的虚拟地址与共享的内存取消映射关系)

    int shmctl(int shmid, int cmd, struct shmid_ds *buf);
    功能:控制/销毁共享内存
    cmd:
    IPC_STAT:获取共享内存的属性
    IPC_SET:设置共享内存的属性
    IPC_RMID:删除共享内存
    IPC_INFO:获取关系内存的信息
    buf:记录共享内存属性的对象

    注意:关系内存是进程间通信方式中最快的一种,因为数据没有复制过程,但是进程之间无法得知数据的写入和读取,需要其它的通信方式配合(信号)

    消息队列的特点是数据可以排队,可以按消息类型接受消息

    信号量可以看作是进程间共享的全局变量,用来管理进程之间共享的资源

    五、消息队列
    1、消息队列是一个由系统内核负责存储和管理、并通过IPC对象标识符获取的数据链表
    #include <sys/types.h>
    #include <sys/ipc.h>
    #include <sys/msg.h>
    int msgget(key_t key, int msgflg);
    功能:创建和获取消息队列
    msgflg:
    创建:IPC_CREAT | IPC_EXEC | 0644
    获取:0

    int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
    功能:向消息队列发送消息
    msgid:msgget的返回值
    msgp:消息(消息类型+消息内容)的首地址
    msgsz:消息内存的长度(不包括消息类型)
    msgflg:
    MSG_NOERROR:当消息的实际长度比msgsz还要长的话则按照msgsz长度截取再发送,否则产生错误

    ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp,int msgflg);
    功能:从消息队列接受消息
    msgp:存储消息的缓冲区
    msgsz:要接收的消息长度
    msgtyp:消息的类型(它包含在消息的前4个字节)
    msgflg:
    MSG_NOWAIT:如果要接收的消息不存在,直接返回,否则消息阻塞等待
    MSG_EXCEPT:从消息队列中接收第一个不msgtyp类型的第一个消息

    int msgctl(int msqid, int cmd, struct msqid_ds *buf);
    功能:控制、销毁消息队列
    cmd:
    IPC_STAT:获取消息队的属性
    IPC_SET:设置消息队列的属性
    IPC_RMID:删除消息队列

    六、IPC相关命令
    ipcs -m 查找共享内容
    ipcrm -m id 删除共享内存
    ipcs -q 察看消息队列
    ipcrm -q id 删除消息队列
    ipcs -s 察看信号量
    ipcrm -s 删除信号量

    七、信号量
    信号量(信号灯),可以当作进程与进程之间共享的全局变量,一般用来为共享的资源计数
    信号量的使用方法:
    1、进程A,创建信号量,并设置初始化(设置资源的数)
    2、进程B,获取信号量,查看信号量(查询剩余资源的数量),减少信号量(使用资源),增加信号量(资源使用完毕归还)
    3、当一进程尝试减少信号量,如果不能减,(资源使用完毕),则进程可以进入等待状态,当信号量能够被减时(其他进程把资源还回来了),进程会被唤醒

    #include <sys/types.h>
    #include <sys/ipc.h>
    #include <sys/sem.h>
    int semget(key_t key, int nsems, int semflg);
    功能:创建信号量或获取信号量
    nsems:信号量的数量
    semflg:
    IPC_CREAT | IPC_EXEC | 0644

    int semop(int semid, struct sembuf *sops, unsigned nsops);
    功能:对信号量增加或减少
    struct sembuf
    {
    unsigned short sem_num; 信号量的编号 /* semaphore number */
    short sem_op; 对信号量的操作 /* semaphore operation */
    short sem_flg; 信号量是否等待 /* operation flags */

    }
    nsops:操作数量

    int semctl(int semid, int semnum, int cmd, ...);
    功能:对信号量控制或释放
    semnum:信号量的编号
    cmd:
    IPC_SET 设置信号量的属性
    IPC_STAT 获取信号量的属性
    IPC_RMID 删除信号量
    IPC_INFO 获取信号量信息
    GETVAL 返回信号量数量
    SETVAL 设置信号量数量
    返回值:信号量的数量

  • 相关阅读:
    大数据量表中,增加一个NOT NULL的新列
    我,属羊...
    打包发布 Qt Quick/Widgets 程序
    C++ 简单的UDP客户端与服务端
    C++ 半同步半异步的任务队列
    键盘鼠标(PS2)模拟器驱动及Demo
    BAT 非右键方式以管理员身份运行批处理
    C++多种方法枚举串口号
    c++ 宏定义调用不定参数的函数
    C++ 调用Python文件方法传递字典参数并接收返回值
  • 原文地址:https://www.cnblogs.com/qsz805611492/p/9409376.html
Copyright © 2011-2022 走看看