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 设置信号量数量
返回值:信号量的数量

原文地址:https://www.cnblogs.com/qsz805611492/p/9409376.html