Linux环境编程 学习笔记 2022年1月8日

201 阅读11分钟

进程的正常退出: 1、在main函数中执行 return n,该返回值值可以被父进程接收到的,在main函数中与exit几乎等价的
2、进程调用了exit函数,该函数是标准库函数
void exit(int status);
功能:在任何时候调用此函数都可以结束进程
status:结束状态码
EXIT_SUCCESS
EXIT_FAILURE
效果与main函数中return的返回值一样
返回值:该函数不会返回

    int atexit(void (*function)(void));
    功能:注册一个进程结束时要执行的函数

    int on_exit(void (*function)(int , void *), void *arg);
    功能:注册一个进程结束时要执行的函数
    

    进程退出前要完成:
        1、先调用通过atexit/on_exit函数注册的函数,注册多个函数时,执行顺序与注册顺序相反
        2、冲刷并关闭所有的打开状态的标准IO流
        3、该函数的实现调用了_exit/_Exit 
3、调用_exit/_Exit函数
    void _exit(int status);
    功能:结束进程,由系统提供
    void _Exit(int status);
    功能:结束进程,由标准库提供
    1、它们的参数都会被父进程获取到
    2、进程结束前会关闭所有打开状态下的文件描述符(系统IO)
    3、会向父进程发送SIGCHLD信号
    4、该函数不会返回
4、进程的最后一个线程执行返回语句时
5、进程的最后一个线程调用了pthread_exit函数

进程的异常终止:
1、进程调用了abort函数,产生SIGABRT信号
2、进程接收到某些信号,可以是其它进程发送的,也可能是自己的错误操作导致的
3、进程的最后一个线程收到“取消”操作,并且还做出响应
这三种结束方式,会让父进程无法获取子进程的结束状态码,所以才叫做异常终止

注意:无论进程是如何结束的,它们最后都会执行同一段代码,关闭所有打开的文件描述符,并释放内存

子进程回收: 对于任何结束方式,都希望能够让父进程知道,通过wait、waitpid函数可以知道子进程是如何结束以及结束状态码 pid_t wait(int *status); 功能:等待子进程结束,并获取结束状态码 返回值:结束的子进程的ID 1、如果所有的子进程都还在运行,则阻塞 2、如果有一个子进程结束,立即返回该子进程的结束状态码和PID 3、如果没有子进程则立即返回 -1

    WIFEXITED(status)   判断进程是否是正常结束,如果是返回真
    WEXITSTATUS(status) 如果进程是正常结束,才可以获取到正确的结束状态码,只获取低8WIFSIGNALED(status) 判断进程是否异常结束,如果是则返回真
    WTERMSIG(status)    如果进程是异常结束,可以获取到杀死进程的信号

    注意:由于wait函数可能会发生阻塞,因此不适合在正常的业务逻辑中调用该函数,可以为SIGCHLD信号注册一个信号处理函数,
    在处理函数中调用wait,因为子进程结束后都会向父进程发送该信号

pid_t waitpid(pid_t pid, int *status, int options);
功能:指定回收某个或某些进程
pid:
    < -1    等待abs(pid) 进程组中的进程结束 -10 10
    -1      等待任意子进程结束,功能与wait等价
    0       等待同组的任意子进程结束
    >0      等待该进程结束
status:结束状态码,与wait的等价
options:
    WNOHANG 非阻塞模式,如果要等待的进程没有一个结束,立即返回
    WUNTRACED   如果有进程处于暂停状态,则返回该进程的状态
    WCONTINUED  如果有进程从暂停转为继续运行,则返回该进程的状态

    WIFSTOPPED(status)  判断进程是否处于暂停状态,如果是返回真
    WSTOPSIG(status)    如果有进程处于暂停状态,则返回导致暂停的信号
    WIFCONTINUED(status)    判断进程是否是由暂停转为继续运行,如果是返回真


int system(const char *command);
功能:执行一个可执行文件,本质其实是创建一个子进程去加载该可执行文件
返回值:子进程结束后才返回
该函数的实现底层调用了fork、exec系列函数、waitpid函数,其实让进程创建了一个子进程,然后子进程加载了command可执行文件

进程间通信
基本概念:
什么是进程间通信:
是指两个或多个进程之间交互数据的过程,是因为进程之间是相互独立的,为了协同工作的需要必须要交互数据
进程间通信的分类:
简单的进程间通信:信号、文件、环境变量、命令行参数
传统的进程间通信:管道文件
XSI进程间通信:共享内存、消息队列、信号量
网络进程间通信:套接字Socket

传统进程间通信-管道(FIFO):
管道是UNIX系统中最古老的进程间通信方式,古老意味着所有系统都支持,早期的管道文件都是半双工,现有的一些系统的管道是全双工的
管道是一种特殊的文件,它的数据在文件中是流动的,读取之后就会消失,如果文件中没有任何数据读取时会阻塞

有名管道:基于有文件名的管道文件的通信
    编程模型:
        进程A               进程B
        创建管道
        打开管道            打开管道
        写数据              读数据
        关闭管道            关闭管道
        删除管道
    创建管道文件:
        1、mkfifo filename
        2、函数 
        int mkfifo(const char *pathname, mode_t mode);
        功能:创建有名管道文件
        pathname:管道文件路径
        mode:管道文件权限

匿名管道:
    注意:匿名管道只适合fork创建的父子进程之间使用
    int pipe(int pipefd[2]);
    功能:创建一个匿名管道文件,返回管道文件的读权限fd和写权限fd
    pipefd:返回用于存储管道文件读写fd的数组,输出型参数
        pipefd[0] 用于读
        pipefd[1] 用于写
    编程模型: 
        父进程              子进程
        获取一对fd          共享了一对fd
        关闭读              关闭写
        写数据              读数据
        关闭写              关闭读    

XSI进程间通信:
X/Open公司制定用于进程间通信的系统接口
XSI进程间通信技术都需要借助系统内核,需要创建内核对象,内核对象会以整数形式返回给用户态,相当于文件描述符,也叫做IPC标识符
文件的创建打开需要借助文件名,同样的,IPC内核对象创建需要借助IPC键值(整数),必须要确保IPC键值是独一无二的

key_t ftok(const char *pathname, int proj_id);
功能:计算出一个独一无二的IPC键值
pathname:项目路径,不是依靠字符串计算,而是依靠路径的位置以及项目编号计算的,所以不能提供假路径,否则可能会产生同样的IPC键值
proj_id:项目编号
返回值:计算出来的IPC键值

共享内存:
基本特点:
两个或者多个进程之间共享一块由内核负责维护的内存,该内存可以与多个进程的虚拟内存进行映射
优点:不需要复制信息,是一种最快的IPC通信机制
缺点:需要考虑同步访问的问题,一般借助信号来解决

    int shmget(key_t key, size_t size, int shmflg);
    功能:创建/获取共享内存
    key:由进程提供的一个独一无二的IPC键值
    size:共享内存的大小,获取时该参数无意义,一般给0
    shmflg:
        IPC_CREAT   创建共享内存
        IPC_EXCL    共享内存如果已存在,则返回错误
        获取时直接给0
        mode_flags  创建共享内存时需要提供权限 IPC_CREAT|0644
    返回值:IPC标识符,错误时返回-1

    void *shmat(int shmid, const void *shmaddr, int shmflg);
    功能:让虚拟内存和共享内存进行映射
    shmid:IPC标识符
    shmaddr:想要映射的虚拟内存首地址,为NULL时系统会自动操作
    shmflg:
        SHM_RDONLY:以只读方式映射共享内存
        SHM_RND:只有当shmaddr参数不为NULL时有效,表示对shmaddr参数向下取内存页的整数倍,作为映射地址
        如果都不需要,则写0

    返回值:与共享内存映射后的虚拟内存的首地址,失败返回(void*)-1 或者 0xFFFFFFFF

    int shmdt(const void *shmaddr);
    功能:取消映射
    shmaddr:映射过的虚拟内存首地址

    int shmctl(int shmid, int cmd, struct shmid_ds *buf);
    功能:删除/控制共享内存
    shmid:IPC标识符
    cmd:
        IPC_STAT    获取共享内存的属性 则buf为输出型参数
        IPC_SET     设置共享内存的属性 则buf为输入型参数
        IPC_RMID    删除共享内存       则buf给NULL
    buf:
    返回值: 成功0 失败-1
    struct shmid_ds {
        struct ipc_perm shm_perm;    // 所有者的相关信息
        size_t          shm_segsz;   // 共享内存的字节数
        time_t          shm_atime;   // 最后映射时间
        time_t          shm_dtime;   // 最后取消映射时间
        time_t          shm_ctime;   // 最后改变的时间
        pid_t           shm_cpid;    // 创建者的进程号
        pid_t           shm_lpid;    // 最后映射、取消映射者的进程号
        shmatt_t        shm_nattch;  // 当前映射的次数
        ...
    };

    struct ipc_perm {
        key_t          __key;    // 创建共享内存的IPC键值
        uid_t          uid;      // 当前使用共享内存的用户ID
        gid_t          gid;      // 当前使用共享内存的组ID/
        uid_t          cuid;     // 创建共享内存的用户ID
        gid_t          cgid;     // 创建共享内存的组ID
        unsigned short mode;     // 共享内存的权限
        unsigned short __seq;    // 共享内存的序列号
    };

编程模型:
    进程A                   进程B 
    创建共享内存            获取共享内存 
    映射共享内存            映射共享内存 
    写数据并通知其它进程     接收到了通知后读数据
    接收到了通知后读数据     写数据并通知其它进程
    取消映射                取消映射
    删除共享内存 

信息队列:
基本特点:
由内核维护管理的数据链表,是通过消息类型收发数据
int msgget(key_t key, int msgflg);
功能:创建/获取消息队列
key:IPC键值
msgflg:
IPC_CREAT 创建消息队列
IPC_EXCL 如果消息队列已存在则返回错误
mode: 当创建消息队列时需要提供权限
返回值:成功返回IPC标识符,失败返回-1

int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
功能:向消息队列发送数据
msqid:IPC标识符
msgp:要发送的消息的首地址
    struct msgbuf {
        long mtype;       // 消息类型
        char mtext[n];    // 数据
    };
msgsz:数据的字节数,是不包含消息类型的
msgflg:
    阻塞一般写0
    IPC_NOWAIT  当消息队列满时,不等待立即返回

ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp,int msgflg);
功能:从消息队列中读取数据
msqid:IPC标识符
msgp:存储数据结构体首地址
msgsz:数据结构体的字节数
msgtyp:要读取的消息类型,是按照类型的值来读取消息,而不是按照顺序
    >0 读取消息队列中第一条等于msgtyp的消息
    =0 读取消息队列中的第一条消息
    <0 读取消息类型小于abs(msgtyp)的消息,如果有多个满足则读取值最小的
msgflg:
    IPC_NOWAIT  消息类型不符合时不阻塞,立即返回
    MSG_EXCEPT  如果msgtyp>0,则读取第一个消息类型不是msgtyp的消息
    MSG_NOERROR 如果不包含此标记,则消息的实际长度>msgsz,则会返回错误,并且读取失败,如果包含此标记,
    则最多读取msgsz个字节,确保一定成功
返回值:成功读取到的字节数

int msgctl(int msqid, int cmd, struct msqid_ds *buf);
功能:删除/控制消息队列
msqid:IPC标识符
cmd:
    IPC_STAT    获取消息队列的属性 则buf为输出型参数
    IPC_SET     设置消息队列的属性 则buf为输入型参数
    IPC_RMID    删除消息队列       则buf给NULL

buf:
    struct msqid_ds {
        struct ipc_perm msg_perm;     // 属主信息
        time_t          msg_stime;    // 最后发送时间
        time_t          msg_rtime;    // 最后接收时间
        time_t          msg_ctime;    // 最后修改时间
        unsigned long   __msg_cbytes; // 当前消息队列字节数
        msgqnum_t       msg_qnum;     // 当前消息的数量
        msglen_t        msg_qbytes;   // 消息的最大字节数
        pid_t           msg_lspid;    // 最后发送者的PID
        pid_t           msg_lrpid;    // 最后接收者的PID
    };

编程模型:
    进程A               进程B
    创建消息队列        获取信息队列
    发送消息            接收消息
    接收消息            发送消息
    删除消息队列

基于进程间通信的银行系统

主要分两个大块:(C_S模型)

客户端(Client):
1、进入时的功能:开户、销户、登录、解锁
开户:输入姓名、身份证号、设置密码,如果开户成功,则在服务器端保存一个账号信息的文件(一个账号一个文件,文件名可以用账号)
销户:输入账号、密码、服务器询问是否确认销户,如果确认则服务器删除该账户的文件
登录:输入账号、密码,如果三次错误锁定账号
解锁:输入账号、身份证号,解锁
2、登录成功:存钱、取钱、转账、查询、修改密码
存钱:输入金额,存入
取钱:输入金额,取出
转账:输入目标账号和转账金额
查询:不需要输入数据
修改密码:原密码和新密码

服务端(Server):
开启服务各项功能的子进程即可:
各项的子进程按照信息类型来接收消息

推荐使用的技术:
    vfork + 消息队列 + exec系列函数

账号结构体:
{
账号
身份证号
密码
金额
是否锁定
}

CTOS消息结构体:
{
消息类型
账号结构体
进程号
}

STOC消息结构体:
{
消息类型
提示信息
}