进程 信号 通信 消息队列 共享内存 进程间通信 等读书笔记及个人小练习

一、基本概念
 1、进程与程序
 程序是存储在磁盘上的文件,它是包含要执行的机器指令和数据的静态实体。
 进程是一个正在运行的程序,一个程序可能包含多个进程(多任务、多进程),进程在操作系统中是一个执行任务的单位。
 
 2、进程的分类
 交互进程:需要用户输入数据,也会显示一些结果给用户看。
 批处理进程:用来执行脚本的进程,例如Makefil。
 守护进程:它是一种一直活跃的进程,一般是后台的,由操作系统启动时通过开过开机脚本加载或由超级用户加载。
 
 3、查看进程
 ps:显示当用户当前终端所控制的进程。
 -a:显示所有用户的进程
 -x:包括无终端控制的进程
 -u:显示详细信息
 -w:以更宽的方式显示
 
 4、进程信息表
 USER:属主
 PID:进程号
 %CPU:cpu占用率
 %MEM:内存使用率
 VSZ:虚拟内存的大小
 RSS:物理内存的使用量
 TTY:终端设备号,如果不是终端控制进程用'?'表示。
 STAT:终端的状态
 START:开始时间
 TIME:运行时间
 COMMAND:开启此进程的命令
 
 5、进程的状态
 O:就绪态,一切准备工作都已经做好,等待被调用。
 R:运行态,Linux下没有就绪态,O也就是R。
 S:可唤醒的睡眠态,系统调用、获取到资源、收到信息都可以被唤醒。
 D:不可唤醒的睡眠态,必须等到的事件发生。
 T:暂停态,收到了SIGSTOP信号,当收到SIGCONT信号则继续运行。
 X:死亡态。
 Z:僵尸态。
 <:高优先级。
 N:低优先级。
 L:多线程进程。
 s:有子进程的进程。
 +:后台进程组。
 
 6、父子进程
  如果进程B是由进程A开启的,那么我们把进程A叫进程B的父进程,进程B叫作进程A的子进程。
  当子进程结束后会向父进程发送,SIGCHLD,父进程收到信号后再支回收子进程。
  当先父进程先结束,子进程就会变成孤儿进程,会被孤儿院(init)收养。
  如果子进程已经结束,但父进程没有及时回收,子进程就变成了僵局进程。 
 
 7、进程标识符,
  俗称进程号,它是一个无符号整数,使用getpid函数可以获取到。
  这种编号是循环使用,当进程结束后,它就跟这个编号没有关系,这个编号也会被再次使用。
  延时重用。
 
二、getxxid
 pid_t getpid(void);
 功能:获取进程id
 
 pid_t getppid(void);
 功能:获取父进程id
 
 uid_t getuid(void);
 功能:获取实际调用者的用户id,
 
    uid_t geteuid(void);
    功能:获取程序的拥有者用户的id
 
 gid_t getgid(void);
 功能:获取实际使用者的组id,
 
    gid_t getegid(void);
    功能:获取程序的拥有者用户组id
    
    1、切换用户
    2、chmod u+s a.out
    3、chmod g+s a.out

 


   
    进程在运行时会根据实际的调用都来获取它的权限,这样可以让程序比实际的拥有者还要高的权限。
   
三、fork
 pid_t fork(void);
 功能:创建子进程
 
 1、失败返回-1,如果成功会返回两次。
 2、父进程会返回子进程的id,子进程返回0
 3、根据返回值的不同,分别为子进程和父进程设计不同的分支。
    4、通过fork创建出的子进程,就是父进程的副本,它会把父进程的堆、栈、全局段、静态数据段、IO流的缓冲区都拷贝一份,父子进程共享代码段。
    5、fork函数调用成功后,父子进程就开始各自执行了,它们的先后顺序是不确定的,但可以通过某些实现来保证。
    
    练习1:编写一个程序,让此程序拥有5个进程同时执行。
 练习2:编写一个程序,让此程序有三级子进程。
 
 6、当总进程数超过系统的限制时,就无法再创建进程的了,此时fork函数会返回-1,执行失败。
 
 7、孤儿进程:子进程还有运行,但父进程已经结束,此时子进程会被init(1)收养,子进程变成孤儿进程。
 8、僵尸进程:子进程已经死亡,但父进程没有及时回收子进程,此时子进程就会变成僵尸进程。
 9、父进程打开的文件和子进程是共享的。
 注意:fork之前的代码,只有父进程在执行,fork之后的代码父子进程都有机会执行,根据fork返回值来控制进入不同的分支。
 
四、vfork
 pid_t vfork(void);
 功能:创建子进程
 
 1、vfork不能单独创建子进程,需要与excl函数簇,配合才完成子进程的创建。
 2、它不会复制父进程的栈、堆、数据、全局等段,也不会共享代码段,而是通过excl函数调用一个程序直接启动,从面提高创建进程的效率。 
 3、使用vfork创建的子进程保证,先执行子进程,后执行父进程。
 
五、进程的正常退出
 1、从main退出,在main中执行return 0;
  返回值的低8位会被父进程获取到。
  
 2、系统的_exit(stat)/标准的_Exit(stat),这两个函数几乎没有什么区别。
 #include <unistd.h>
 void _exit(int status);
 #include <stdlib.h>
 void _Exit(int status);
 a、返回值的低8位会被父进程获取到。
 b、使用_exit/_Exit退出前会关闭所有打开的文件流,如果有子进程则会托附给init,然后向父进程发送SIGCHLD信号。
 c、此函数不会返回。
  
 3、调用标准C的exit(stat)函数
 a、exit在底层实现上调用了_exit/_Exit函数,所以_exit/_Exit的特点它都具备。
 b、exit结束前会调用通过atexit/on_exit注册的函数。
 
 int atexit(void (*function)(void));
 int on_exit(void (*function)(int , void *), void *arg);
 
 4、最后一个线程正常结束
 
六、进程异常中止
 1、进程调用了abort函数(段错误、浮点异常)
 2、进程接收到某些信号
  crtl+c
  crtl+\
  ctrl+z
 3、最后一个线程收到取消操作,而且线程作出响应。
 
七、子进程的回收
 pid_t wait(int *status);
 功能:等待子进程结束,并回收
 
    pid_t waitpid(pid_t  pid,  int  *status,int options);
    功能:等待指定的子进程结束,并回收
   
    1、当一个进程结束时,内核会向它的父进程发送一个信号
     父进程收到这个信号后可以指定一个函数来处理,也可以选择忽略,默认情况下是忽略的。
     
    2、父进程调用wait调用才是有意义
     如果所有的子进程都在运行,则父进程阻塞(wait)
     只要有一个子进程结束了,会立即返回子进程的id和结束状态。
     当所有子进程都结束运行时,wait会返回-1。
     如果在调用wait之前子进程就已经结束(僵尸子进程),执行wait函数时会立即返回并回收僵尸进程。
     
    3、waitpid函数可以指定等于哪个子进程
     pid:指定的pid
      == -1 功能与wait类似,pid就无意义了。
      > 0 等待进程号是pid的进程结束。
      
      == 0 等待组id等于pid的进程组中任意进程结束。
      < -1 等待组id是pid的绝对值的进程组中任意进程结束。
      
     status:用于接收子进程的结束状态,如果不需要状态码可以设置为NULL;
     
     options:
      0 以阻塞状态等待子进程结束
      WNOHANG 如果没有子进程退出会立即返回。
      WUNTRACED 等待的进程处于停止状态,并且之前没有报告过,则立即返回。
      
    4、如果不调用wait/waitpid函数,子进程结束后就处于僵尸状态,当父进程也结束时,父进程的父进程会把他们统一回收。
     
八、exec函数簇
 exec函数的功能:加载一个可执行文件,要和vfork函数配合才有意义。
 
 int execl(const char *path,  const  char
       *arg, ...);
    path:可执行文件的路径
    arg:给可执行文件的参数,类似于命令行参数,必须以NULL结尾,第一个必须是可以执行文件名。
    execl("","a.out",NULL);
   
    1、通过exec创建的子进程会替换掉父进程给的代码段,不拷贝父进程的堆、栈、全局、静态数据段,会用新的可执行文件替换掉他们。
    2、exec只是加载一个可执行文件,并不创建进程,不会产生新的进程号。
    3、只有exec函数执行结束(无论成功还是失败),父进程才能继续执行。
 int  execlp(const char *file, const char
       *arg, ...);
    file:可执行文件的文件名,会从PATH环境变量指定的位置去找可执行文件。
       
    int execle(const char *path, const  char
       *arg,..., char * const envp[]);
       
    int  execv(const char *path, char *const
       argv[]);
     
    int execvp(const char *file, char *const
       argv[]);

 

 
一、基本的概念
 1、中断
 中止、暂停当前正在执行的进程,转而去执行其它的任务。
 硬中断:来自硬件设备的中断
 软中断:来自其它程序的中断
 
 2、信号
 信号是一种软中断,可以把他看作是进程与进程、内核与进程通信的一种方式,它为进程的异步执行,提供了技术支持。
 
 3、常见的信号
  SIGINT(2) 终端中断信号Ctrl+c
  SIGQUIT(3) 终端退出信号Ctrl+/ 
  SIGABRT(6) 调用abort函数产生的信号
  SIGFPE(8) 算术信号
  SIGKILL(9) 死亡信号
  SIGSEGV(11) 段错误信号
  SIGALRM(14) 闹钟信号
  SIGCHLD(17) 子进程结束信号
  SIGCONT(18) 进程继续信号
  SIGSTOP(19) 进程暂停信号
  SIGTSTP(20) 终端停止信号

 


  
 4、不可靠信号(非实时)
  1、编号小于SIGRGMI(34)的信号都是不可靠的,这些信号是建立在早期的信号机制上的,一个事件发生可能会产生多次信号。
  2、不可靠信号不支持排除,在接收信号的时候可能会丢失,如果一个发给一个进程多次,它可能只接收到一次,其它的可能就丢失了。 
  3、进程在处理这种信号的时候,哪怕设置的信号处理函数,当信号处理函数执行完毕后,会再次恢复成默认的信号处理方式。
  
 5、可靠信号(实时)
  1、位于[SIGRGMI(34),SIGRTMAX(64)]区间的都是可靠信号。
  2、可靠信号支持排除,不会丢失。
  3、无论是可靠信号还是不可靠信号都是通过:kill、signal、sigqueue、sigaction进行处理。
 
 6、信号的来源
  硬件来源:
   键盘:Ctrl+c、Ctrl+/、Ctrl+z
   驱动:硬件设备被激活、使用、失效。
   内存:非法访问内存。
  软件来源:
   命令:kill、killall
   函数:kill/raise/alarm/setitimer/sigqueue。
   
 7、信号的处理方式
  1、忽略
  2、终止
  3、终止+core
  core dump 把内存的使用情况扔出来。
  core文件是一种二进制文件,需要一些高度工具才能解析出来(gdb)。
  1、gcc -g code.c -> 生成带调试信息的可执行文件。
  2、运行可执行文件产生core文件
  3、gdb ./a.out core 程序会停止在产生错误的位置。
  
  ubuntu默认不产生core谁的,需要使用命令设置:
   ulimit -c unlimited
  
  4、捕获并处理
二、信号的捕获和处理
 #include <signal.h>
    typedef void (*sighandler_t)(int);
    sighandler_t  signal(int  signum, sighan‐
       dler_t handler);
    功能:向注册一个信号处理函数
    signum:信号的编号,可以直接写数字,也可以使用系统提供的宏。
    handler:函数指针
     SIG_IGN 忽略信号
     SIG_DFL 恢复信号默认的处理方式
    返回值:是之前信号处理方式
     函数指针、SIG_IGN、SIG_DFL、SIG_ERR

 

   
    练习:实现一个杀不死的程序。
    1、在有些系统中向内核注册的信号处理函数只执行一次(在执行前就被恢复成默认的处理方式),如果想持续处理信号,可以在每次的处理函数结束时再次注册。
   
    2、SIGKILL、SIGSTOP不能被捕获、也不能被忽略。
   
    3、普通用户只能给自己的进程发信号,超级用户可以能任意进程发送信号。
     
三、子进程的信号处理
    1、通过fork创建的子进程会继承父进程的信号处理方式。
    2、通过vfork+exec创建的子进程不会继承父进程的信号处理方式,会恢复成默认的。
    练习:测试通过vfork+exec创建的子进程是否会继承父进程的信号处理方式。
   
四、信号的发送
 1、键盘
  Ctrl+c 终端中断信号
  Ctrl+z 终端暂停信号,fg命令再次开启
  Ctrl+/ 终端退出信号
 2、错误产生的信号
  除0
  非法内存访问
  硬件总线
 3、命令产生的信号
  kill -信号 进程号
  killall -信号 程序名(杀死所有同名的进程)。
 4、函数产生的信号
  int kill(pid_t pid, int sig);
  功能:向指定的进程发送信号
  pid:与waitpid一样
  sig:信号
   0表示空信号,不会向进程发送信号,但是会测试是否能向pid发送信号,这样可以检测一个进程是否存在,返回-1表示进程不存在,errno为ESRCH。
  返回值:-1,说明进程不存在
  
  int raise(int sig);
  功能:向自己发送信号
 
 练习:实现kill命令的功能。
 
五、pause
 #include <unistd.h>
    int pause(void);
    功能:休眠
   
    1、进程调用了pause函数后会进程睡眠状态,直到有信号把它叫醒(不被忽略的信号)。
    2、当信号来临后,先执行信号处理函数,信号处理函数结束后pause再返回。
    3、pause函数要么不返回(一直睡眠),要么返回-1,并且修改errno的值。
    4、从功能上来讲它相当于没有时间限制的sleep函数。
   
六、sleep
 #include <unistd.h>
 unsigned int sleep(unsigned int seconds);
 功能:使用调用的进程睡眠seconds秒
 
 1、调用sleep的进程如果没有睡眠足够的秒数,除非收到信号后才会返回。
 2、sleep的返回值是0,或剩余的睡眠秒数。
 3、相当于有时间限制的pause
 
 int usleep(useconds_t usec);v
 功能:睡眠usec微秒
 
 1秒=1000毫秒=1000000微秒。
 它是一种更精确的睡眠函数。
 
 -std=c99 不建议使用
 -std=gnu99 在使用系统调用时一定要使用此标准
 
七、alarm
 #include <unistd.h>
    unsigned int alarm(unsigned int seconds);
    功能:定时一个闹钟信号
   
    1、让内核向调用它的进程,在seconds秒后发送一个SIGALRM信号。
    2、SIGALRM信号的默认处理方式是直接退出。
   
    练习:实现一个闹钟命令,./alarm sec
   
八、信号集和信号屏蔽
 1、信号集:
  多个信号的集合,sigset_t
  由128个二进制位组成,每个二进制位表示一个信号
  
  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),它就是一个信号集,里面包含了进程所屏蔽的信号。
  
  int sigprocmask(int how, const  sigset_t
       *set, sigset_t *oldset);
       功能:设置进程的信号掩码(信号屏蔽码)
       how:修改信号掩码的方式
         SIG_BLOCK:向信号掩码中添加信号
         SIG_UNBLOCK:从信号掩码中删除信号
         SIG_SETMASK:用新的信号集替换旧的信号掩码
       newset:新添加、删除、替换的信号集,也可以为空
       oldset:获取旧的信号掩码
       当newset为空时,就是在备份信号掩码
 当进程执行一些敏感操作时不希望被打扰(原子操作),此需要向屏蔽信号。
 屏蔽信号的目的不是为了不接收信号,而是延时接收,当处理完要做的事情后,应该把屏蔽的信号还原。
 当信号屏蔽时发生的信号会记录一次,这个信号设置为末决状态,当信号屏蔽结束后,会再发送一次。
 不可靠信号在信号屏蔽期间无论信号发生多少次,信号解除屏蔽后,只发送一次。
 可靠信号在信号屏蔽期间发生的信号会排队记录,在信号解除屏蔽后逐个处理。
 在执行处理函数时,会默认把当前处理的信号屏蔽掉,执行完成后再恢复
 
 int sigpending(sigset_t *set);
 功能:获取末决状态的信号
 
九、信号处理sigaction signal
 
int sigaction(int signum, const struct sigaction *act,struct sigaction *oldact);
 功能:设置或获取信号处理方式
 
  struct sigaction {
   // 信号处理函数指针
      void (*sa_handler)(int);
      // 信号处理函数指针 需要使用sigqueue发送信号
        void (*sa_sigaction)(int, siginfo_t *, void *);
        // 信号屏蔽码
        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);

 

 
 
    
一、基本概念
 进程间通信(IPC):进程之间交换数据的过程叫进程间通信。
 进程间通信的方式:
  简单的进程间通信:
   命令行:父进程通过exec函数创建子进程时可以附加一些数据。
   环境变量:父进程通过exec函数创建子进程顺便传递一张环境变量表。
   信号:父子进程之间可以根据进程号相互发送信号,进程简单通信。
   文件:一个进程向文件中写入数据,另一个进程从文件中读取出来。
   命令行、环境变量只能单身传递,信号太过于简单,文件通信不能实时。
  
  XSI通信方式:X/open 计算机制造商组织。
   共享内存、消息队列、信号量
  网络进程间通信方式:
   网络通信就是不同机器的进程间通信方式。
  
  传统的进程间通信方式:管道
二、管道
 1、管道是一种古老的通信的方式(基本上不再使用)
 2、早期的管道是一种半双工,现在大多数是全双工。
 3、有名管道(这种管道是以文件方式存在的)。
 int mkfifo(const char *pathname, mode_t mode);
   
 管道通信的编程模式:
  进程A    进程B
  创建管道mkfifo
  打开管道open   打开管道
  写/读数据read/write 读/写数据
  关闭管道close   关闭管道
   
 4、无名管道:由内核帮助创建,只返回管道的文件描述符,看不到管道文件,但这种管道只能用在fork创建的父子进程之间。
  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 
  创建共享内存  
  加载共享内存 加载共享内存
  卸载共享内存 卸载共享内存
  销毁共享内存
 
 int shmget(key_t key, size_t size, int shmflg);
 功能:创建共享内存
 size:共享的大小,尽量是4096的位数
 shmflg:IPC_CREAT|IPC_EXCL
 返回值:IPC对象标识符(类似文件描述符)
 
 void *shmat(int shmid, const void *shmaddr, int shmflg);
 功能:加载共享内存(进程的虚拟地址与共享的内存映射)
 shmid:shmget的返回值
 shmaddr:进程提供的虚拟地址,如果为NULL,操作系统会自动选择一块地址映射。
 shmflg:
  SHM_RDONLY:限制内存的权限为只读
  SHM_REMAP:映射已经存的共享内存。
  SHM_RND:当shmaddr为空时自动分配
  SHMLBA:shmaddr的值不能为空,否则出错
 返回值:映射后的虚拟内存地址
  
 int shmdt(const void *shmaddr);
 功能:卸载共享内存(进程的虚拟地址与共享的内存取消映射关系)
 
 int shmctl(int shmid, int cmd, struct shmid_ds *buf);
 功能:控制/销毁共享内存
 cmd:
  IPC_STAT:获取共享内存的属性
  IPC_SET:设置共享内存的属性
       IPC_RMID:删除共享内存
    buf:
     记录共享内存属性的对象
     
五、消息队列
 1、消息队列是一个由系统内核负责存储和管理、并通过IPC对象标识符获取的数据链表。
 
 int msgget(key_t key, int msgflg);
 功能:创建或获取消息队列
 msgflg:
  创建:IPC_CREAT|IPC_EXEC
  获取:0
  
 int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
 功能:向消息队列发送消息
 msqid: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:删除消息队列
 
 
    
     
  
   
  
 
posted @ 2018-08-07 20:47  Scott9527  阅读(520)  评论(0编辑  收藏  举报