linux之进程线程信号全解
发布日期:2021-06-29 11:09:19 浏览次数:2 分类:技术文章

本文共 22982 字,大约阅读时间需要 76 分钟。

一、linux进程全解

1、程序的开始结束及预处理atexit函数

main函数由谁调用;其实在裸机程序中c语言运行前是需要一段引导的汇编代码为c语言运行准备环境的如栈,只是在使用编译器开发过程中我们不需要关注,因为编译器已经在编译链接的时候做了处理。

执行程序可以使用exec族函数来执行

argc和argv参数,其实是在运行的时候在命令行shell命令执行的时候传参传入的,再启动代码传递给main函数。

程序结束

正常终止;return, exit ,_exit
非正常终止;自己或他人发信号终止进程,如ctrl+c

atexit函数 注册进程终止时需要调用的处理函数,就是在程序结束前做的预处理。

int atexit(void (*function(void))),注册一个函数在进程终止前会调用执行他。

注意;atexit注册多个进程终止处理函数,先注册的后执行(先进后出,和栈一样)

return和exit效果一样,都是会执行进程终止处理函数,但是用_exit终止进程时并不执行atexit注册的进程终止处理函数。

2、进程环境

2.1、环境变量

export命令查看环境变量、echo $环境变量 方式也可以打印环境变量的值

进程环境变量表,之前linuxIO文件中有说过,就是每个进程都有一个他所有环境变量的表格,并且在程序中是可以去使用这些环境变量的,(但是注意,如果程序中使用了环境变量那么该程序就是与环境有关的程序了,如jdk这些是需要配置环境变量才能使用的)

在程序中可以直接通过声明 extern char **environ;从而在程序中就可以使用了环境变量了。与uboot中使用环境变量一样,获取使用getenv、设置使用setenv、但是这里的修改只能修改自己当前进程的进程环境表的值。

int main(void){
extern char **environ; // 声明就能用 int i = 0; while (NULL != environ[i]) {
printf("%s\n", environ[i]); i++; } return 0;}

2.2、进程运行的虚拟地址空间

操作系统中每个进程在独立地址空间中运行,每个进程的逻辑地址空间均为4G(32位)存在虚拟地址到物理地址的映射。

3、进程的引入

3.1、进程相关介绍

进程就是程序的一次运行过程

PCB;进程控制块;内核中专门用来管理一个进程的数据结构

进程ID;用来唯一标识我们的一个进程

其中有一系列获取进程ID的API;getpid获取自己id,getppid获取父进程ID,getuid、geteuid、getgid,getegid

多进程的调用原理,这个就是实现进程运行时微观的串联和宏观的并行

3.2、fork引入

3.2.1、为什么需要fork,而非创建新进程

因为创建一个进程则需要重新新建一个进程环境表以及PCB进程控制块,这个都是很需要效率的,因此操作系统引入fork进行复制而非重新创建,复制后对PCB进行修改从而达到新进程的效果,并且也减少了大量的开销。

在这里插入图片描述

3.2.2、fork内部原理

pid_t fork(void) fork函数调用一次会返回2次,返回值等于0的为子进程,大于0的为父进程,然后再根据返回值来分别对父子进程做不同的处理分支。

fork之后的父子进程都有自己单独的PCB,是独立的两个进程了。

#include 
#include
#include
int main(void){
pid_t p1 = -1; p1 = fork(); // 返回2次 if (p1 == 0) {
// 这里一定是子进程 // 先sleep一下让父进程先运行,先死 sleep(1); printf("子进程, pid = %d.\n", getpid()); printf("hello world.\n"); printf("子进程, 父进程ID = %d.\n", getppid()); } if (p1 > 0) {
// 这里一定是父进程 printf("父进程, pid = %d.\n", getpid()); printf("父进程, p1 = %d.\n", p1); } if (p1 < 0) {
// 这里一定是fork出错了 } // 在这里所做的操作 //printf("hello world, pid = %d.\n", getpid()); return 0;}

3.2.3、父子进程关系

父子进程其实还是有一些关联的,

父进程在没有fork之前对自己的事情对于子进程有很大影响(例如在fork之前父进程打开一个文件,则后面父子进程同时写入该文件是接续写的,因为其文件指针是有关联的是同步的)
但是在fork之后两边做的操作都是独立的,没有影响的(例如在fork之后父子进程打开一个文件同时写入该文件是分别的,因为其文件指针是没有关联的)

注意;子进程最终目的都是要独立去运行另外的程序

4、进程的诞生和消亡时的资源管理

4.1、进程的诞生

进程0是操作系统内核启动过程中手动构建起来的,也就是PCB结构体中的所有元素都是内核手动一个个赋值的,

进程1就是开始由进程0利用内部fork而来的没知识当时进程1在内核态,只有执行根文件系统下的init文件才最终变成应用态我们所使用的的进程1,
之后的所有进程都是由进程1fork直接或间接而来的

4.2、进程的消亡

进程的消亡也分为正常终止和非正常终止

进程相关的资源有两部分;进程运行时使用的一系列资源、进程本身PCB和本身栈占用的一部分内存。

在linux内核设计过程中其中进程运行时使用的一系列资源都有操作系统自动回收,但是不会管其本身的资源PCB,则这部分资源需要我们单独处理,就是交给其父进程进行回收

4.3、僵尸进程和孤儿进程

僵尸进程;就是子进程先与父进程结束,但父进程还未来得及去回收子进程本身的资源,这个时候子进程就成为僵尸进程。

父进程可以使用wait或waitpid以显示回收子进程本身资源,也可以当父进程自己结束时也会自动回去剩余的待回收资源(这样设计就是怕设计者忘记回收了造成内存泄露)

孤儿进程;就是父进程先与子进程结束,则子进程本身资源就没有人回收,从而成为孤儿进程

但是linux内核设计,最后所有的孤儿进程都会自动成为进程1init进程的子进程。

ps -aux命令用于展示操作系统所有进程

4.4、父进程wait回收子进程

wait的工作原理 (1)子进程结束时,系统向其父进程发送SIGCHILD信号 (2)父进程调用wait函数后阻塞

(3)父进程被SIGCHILD信号唤醒然后去回收僵尸子进程
(4)父子进程之间是异步的,SIGCHILD信号机制就是为了解决父子进程之间的异步通信问题,让父进程可以及时的去回收僵尸子进程。
(5)若父进程没有任何子进程则wait返回错误

pid_t wait(int *status)

status 输出型参数 用来返回子进程结束时的状态,看子进程是正常结束还是异常终止的,有特定的宏来表示的,
WIFEXITED宏用来判断子进程是否正常终止(return、exit、_exit退出)
WIFSIGNALED宏用来判断子进程是否非正常终止(被信号所终止)
WEXITSTATUS宏用来得到正常终止情况下的进程返回值的。

pid_t返回值 用于返回此致回收子进程的pid,因为当前父进程可能有多个子进程,则可以需要判断回收的子进程是否是特定的,则需要子进程pid进行判断。

总结;wait主要是用来回收子进程资源,回收同时还可以得知被回收子进程的pid和退出状态。

#include 
#include
#include
#include
#include
int main(void){
pid_t pid = -1; pid_t ret = -1; int status = -1; pid = fork(); if (pid > 0) {
// 父进程 //sleep(1); printf("parent.\n"); ret = wait(&status); printf("子进程已经被回收,子进程pid = %d.\n", ret); printf("子进程是否正常退出:%d\n", WIFEXITED(status)); printf("子进程是否非正常退出:%d\n", WIFSIGNALED(status)); printf("正常终止的终止值是:%d.\n", WEXITSTATUS(status)); } else if (pid == 0) {
// 子进程 printf("child pid = %d.\n", getpid()); return 51; //exit(0); } else {
perror("fork"); return -1; } return 0;}

还有一个waitpid函数也是用于回收子进程的,只是他可以指定需要回收子进程的pid,并且还可以选择模式是否堵塞,而wait是不能指定pid并且只能是堵塞的。

pid_t waitpid(pid_t pid,int *status,int potions)
pid传参子进程PID,如果不指定则传入-1即可
potions 模式选择 0表示阻塞,WNOHANG表示非阻塞

#include 
#include
#include
#include
#include
int main(void){
pid_t pid = -1; pid_t ret = -1; int status = -1; pid = fork(); if (pid > 0) {
// 父进程 sleep(1); printf("parent, 子进程id = %d.\n", pid); //ret = wait(&status); //ret = waitpid(-1, &status, 0); //ret = waitpid(pid, &status, 0); ret = waitpid(pid, &status, WNOHANG); // 非阻塞式 printf("子进程已经被回收,子进程pid = %d.\n", ret); printf("子进程是否正常退出:%d\n", WIFEXITED(status)); printf("子进程是否非正常退出:%d\n", WIFSIGNALED(status)); printf("正常终止的终止值是:%d.\n", WEXITSTATUS(status)); } else if (pid == 0) {
// 子进程 //sleep(1); printf("child pid = %d.\n", getpid()); return 51; //exit(0); } else {
perror("fork"); return -1; } return 0;}

4.5、竟态初步引入

竞争状态;就是在多进程环境下,多个进程同时抢占资源从而可能引发的结果不确定性。因此我们应该在写程序的时候尽可能消除竞争状态。

4.6、exec族函数

exec族函数的作用就是为了帮助子进程单独去执行应用程序而产生的。

exec族的6个函数介绍

(1)execl和execv 这两个函数是最基本的exec,都可以用来执行一个程序,区别是传参的格式不同。execl是把参数列表(本质上是多个字符串,必须以NULL结尾)依次排列而成(l其实就是list的缩写),execv是把参数列表事先放入一个字符串数组中,再把这个字符串数组传给execv函数。
//execl("/bin/ls", “ls”, “-l”, “-a”, NULL); // ls -l -a
//char * const arg[] = {“ls”, “-l”, “-a”, NULL};
//execv("/bin/ls", arg);
(2)execlp和execvp 这两个函数在上面2个基础上加了p,较上面2个来说,区别是:上面2个执行程序时必须指定可执行程序的全路径(如果exec没有找到path这个文件则直接报错),而加了p的传递的可以是file(也可以是path,只不过兼容了file。加了p的这两个函数会首先去找file,如果找到则执行执行,如果没找到则会去环境变量PATH所指定的目录下去找,如果找到则执行如果没找到则报错)
(3)execle和execvpe 这两个函数较基本exec来说加了e,函数的参数列表中也多了一个字符串数组envp形参,e就是environment环境变量的意思,和基本版本的exec的区别就是:执行可执行程序时会多传一个环境变量的字符串数组给待执行的程序。
如果用户在执行这个程序时没有传递第三个参数,则程序会自动从父进程继承一份环境变量(默认的,最早来源于OS中的环境变量);如果我们exec的时候使用execlp或者execvpe去给传一个envp数组,则程序中的实际环境变量是我们传递的这一份(取代了默认的从父进程继承来的那一份)

5、进程状态

5.1、进程各种状态及之间的转换

进程的5种状态

(1)就绪态。这个进程当前所有运行条件就绪,只要得到了CPU时间就能直接运行。
(2)运行态。就绪态时得到了CPU就进入运行态开始运行。
(3)僵尸态。进程已经结束但是父进程还没来得及回收
(4)等待态(浅度睡眠&深度睡眠),进程在等待某种条件,条件成熟后可进入就绪态。等待态下就算你给他CPU调度进程也无法执行。浅度睡眠等待时进程可以被(信号)唤醒,而深度睡眠等待时不能被唤醒只能等待的条件到了才能结束睡眠状态。
(5)暂停态。暂停并不是进程的终止,只是被被人(信号)暂停了,还可以回复的。

进程各种状态之间的转换图

在这里插入图片描述

5.2、system函数

system函数 = fork+exec

其是原子操作。原子操作意思就是整个操作一旦开始就会不被打断的执行完。原子操作的好处就是不会被人打断(不会引来竞争状态),坏处是自己单独连续占用CPU时间太长影响系统整体实时性,因此应该尽量避免不必要的原子操作,就算不得不原子操作也应该尽量原子操作的时间缩短。

5.3、进程关系

(1)无关系

(2)父子进程关系
(3)进程组(group)由若干进程构成一个进程组
(4)会话(session)会话就是进程组的组
这样的关系相当于建圈,然后在同一个圈中的进程就可以完成这个权限而其他的就不行,这样便于管理。

6、守护进程

6.1、进程相关命令

ps进程查看命令

ps -ajx 显示各种有关的ID号
ps -aux 显示进程各种占用资源

向进程发送信号指令kill

kill -信号编号 进程ID
如kill -9 xxx 则表示向xxx进程发送9号信号,也就是结束该进程

6.2、守护进程Daemon

简称d(进程名后面带d的基本就是守护进程)

守护进程就是与控制台脱离,可以长期运行,一般可以从开机运行到关机,要使用kill -9命令才能结束进程。
服务器程序一般都需要实现为守护进程的

常见的守护进程有

syslogd;系统日志守护进程,提供syslogd功能的
cron;cron进程用来实现操作系统的时间管理,linux实现定时功能就需要用到cron进程

任何一个进程都可以实现成为守护进程,就要自己是否有这个需求。就是是否要成为脱离控制台而需要长期存在的进程,只能使用kill -9命令才能结束进程。

6.3、制作守护进程

(1)子进程等待父进程退出

(2)子进程使用setsid创建新的会话期,脱离控制台
(3)调用chdir将当前工作目录设置为/
(4)umask设置为0以取消任何文件权限屏蔽
(5)关闭所有文件描述符
(6)将0、1、2定位到/dev/null

#include 
#include
#include
#include
#include
#include
void create_daemon(void);int main(void){ create_daemon(); while (1) { printf("I am running.\n"); sleep(1); } return 0;}// 函数作用就是把调用该函数的进程变成一个守护进程void create_daemon(void){ pid_t pid = 0; pid = fork(); if (pid < 0) { perror("fork"); exit(-1); } if (pid > 0) { exit(0); // 父进程直接退出 } // 执行到这里就是子进程 // setsid将当前进程设置为一个新的会话期session,目的就是让当前进程 // 脱离控制台。 pid = setsid(); if (pid < 0) { perror("setsid"); exit(-1); } // 将当前进程工作目录设置为根目录 chdir("/"); // umask设置为0确保将来进程有最大的文件操作权限 umask(0); // 关闭所有文件描述符 // 先要获取当前系统中所允许打开的最大文件描述符数目 int cnt = sysconf(_SC_OPEN_MAX); int i = 0; for (i=0; i

6.4、syslog记录调试信息

守护进程将标准输出都关闭了,则需要引入syslog日志来记录守护进程的信息便于调试,

openlog打开日志、syslog记录日志信息、closelog关闭

void openlog(const char *ident, int option, int facility);

ident用于标志是该进程输出到日志文件中的标志,因为有很多个进程都向日志文件输出的,因此需要ident来区别
option属性,对应一些宏
在这里插入图片描述
void syslog(int priority, const char *format, …);
priority优先等级,与uboot中的print类似也有一个优先等级的,同样在这里priority也有相关的宏对应8个级别,
format表示输出的格式

一般log信息都在操作系统的/var/log/messages这个文件中存储着,但是ubuntu中是在/var/log/syslog文件中的。

int main(void){
printf("my pid = %d.\n", getpid()); openlog("b.out", LOG_PID | LOG_CONS, LOG_USER); syslog(LOG_INFO, "this is my log info.%d", 23); syslog(LOG_INFO, "this is another log info."); syslog(LOG_INFO, "this is 3th log info."); closelog();}

syslog日志记录的工作原理

其实也是守护进程,提供日志服务。
syslogd其实就是一个日志文件系统的服务器进程,提供日志服务。任何需要写日志的进程都可以通过openlog/syslog/closelog这三个函数来利用syslogd提供的日志服务。这就是操作系统的服务式的设计。

注意守护进程应该不能被执行多次,

解决方法是用一个文件是否存在来做标志
第一次执行是创建,之后再次执行判断存在则报错,进程死亡时删除。

//这个名词要古怪一点#define FILE	"/var/zw_test_single111"void delete_file(void);int main(void){
// 程序执行之初,先去判断文件是否存在 int fd = -1; fd = open(FILE, O_RDWR | O_TRUNC | O_CREAT | O_EXCL, 0664); if (fd < 0) {
if (errno == EEXIST) {
printf("进程已经存在,并不要重复执行\n"); return -1; } } atexit(delete_file); // 注册进程清理函数 int i = 0; for (i=0; i<10; i++) {
printf("I am running...%d\n", i); sleep(1); } return 0;}void delete_file(void){
remove(FILE);}

7、linux的进程间通信

7.1、进程间通信

进程间通信IPC值的是2个任意的进程之间的通信,因为虚拟地址的存在,不同进程之前是不知道对方的存在,因此他们进行通信是难的也是为了安全。

7.2、linux提供了多种进程间通信机制

管道(有名管道,无名管道)提供父子进程之间的通信

SystemV IPC信号量,消息队列,共享内存
socket 域套接字 网络通信
信号 进程间发送信号

7.3、IPC对象的持续性

随进程持续性,随内核持续性,随文件系统持续性。

管道、和FIFO都是随进程持续性的,也就是说最后一个将某管道打开着用于读的进程关闭后,内核也将丢弃所有数据并删除该管道。
System V 消息队列、信号量、共享内存都是随内核持续性的,则IPC对象将一直存在直到内核重新自举或者显示删除该对象为止

7.4、IPC对象名字空间的重要性

当两个或多个无亲缘关系的进程使用某种类型的IPC对象来彼此交换信息的时候,该IPC对象必须有一个某种形式的名称或标识符,这样才能在一个进程中创建该IPC对象,在其余进程中指定同一个IPC对象,从而达到通信的效果。(除开管道但是他必须是父子进程间通信的)。名字空间是客服与服务器,多进程直接彼此连接以交换消息的手段。

7.5、IPC对象打开或创建时的oflag值

O_CREAT、不存在则创建,注意还有操作权限的设置问题mode参数。

O_EXCL、一般与O_CREAT一起使用(O_CREAT|O_EXCL)则表示该对象不存在则创建,如果存在则返回一个EEXIST错误。(注意,创建和检查这两部必须是原子的)
O_NONBLOCK、设置为不被堵塞状态,
O_TRUNC、该标志会把原有对象情况从0开始

7.6、管道通信

有pipe创建

7.6.1、实现全双工管道

管道pipe得到的是半双工的,全双工需要两个管道来完成,从pipe[1]写入的数据只能从pipe[0]读出

注意进程的执行顺序

#include 
#include
#include
#include
#include
#include
void server(int, int);void client(int, int);int main(){ int pipe1[2] = { 0}; int pipe2[2] = { 0}; int childpid = 0; char buf[1024] = { 0}; pipe(pipe1);//创建管道,但是管道是半双工的,因此需要裁减 pipe(pipe2); if ( (childpid = fork()) == 0)//fork返回值=0的表示子进程 { close(pipe1[1]);//在子进程这边关闭管道pipe1的写端口 close(pipe2[0]);//在子进程这边关闭管道pipe2的读端口 memset(buf, 0, sizeof(buf)); read(pipe1[0], buf, 1024); printf("%s", buf); write(pipe2[1], "1.hello\n", strlen("1.hello\n")); exit(0);//子进程退出,内核向其父进程发送SIGCHILD信号 } close(pipe1[0]);//在父进程这边关闭管道pipe1的读端口 close(pipe2[1]);//在父进程这边关闭管道pipe2的写端口 //sleep(10); write(pipe1[1], "2.hello\n", strlen("2.hello\n")); memset(buf, 0, sizeof(buf)); read(pipe2[0], buf, 1024); printf("%s", buf); waitpid(childpid, NULL, 0);//阻塞式回收childpid为ID的子进程 return 0;}

7.6.2、ponen和pclose

popen 在调用进程和所指定的命令之间创建一个管道,

FILE *popen(const char * conmmand, const char *type);
type = r;那么调用进程读进command的标准输出
type = w;那么调用进程写入command的标准输入

7.7、FIFO有名管道

与管道本质最大的不同就是,他有名字空间,不需要限定在父子进程之间进程通信,任何两个进程之间都可以使用FIFO进行通信的。

FIFO由mkfifo创建,并且自带O_CREAT|O_EXCL标志,则创建一个新的FIFO要么成功要么返回EEXIST错误,则要使用open打开即可。

FIFO是半双工的,创建后必须打开,并且打开后要么只读要么只写的,并且写只能从后面追加,读只能从开始开始读,不能使用lseek文件指针否则报SEPIPE错误。

write的原子性保证多进程同时写入的时候不会出错。

7.7.1、FIFO实践

//两种情形;

1、分开成两个执行程序,分别执行,先客服端在服务端
2、还可以在客服端中利用fork和exec来调用服务端的功能
注意会卡在open读操作上,直到有对应open的写操作才往下执行,因此不对应的open会造成死锁现象。

#ifndef _UNPIPC_H_#define _UNPIPC_H_//头文件建立fifo文件的文件,通过该文件进行通信的#define FIFO1 "/tmp/fifo.1"#define FIFO2 "/tmp/fifo.2"#endif
//client进程#include 
#include
#include
#include
#include
#include
#include
#include
#include "unpipc.h"#include
int main(){ int readFd = 0; int writeFd = 0; char buf[1024] = { 0}; unlink(FIFO1); unlink(FIFO2); //可以注意一下与管道的区别; //管道只需要pipe即可创建然后在父子进程中使用通信 //而fifo需要mkfifo传入文件和权限以及判断是否创建成功 //再使用open打开 if((mkfifo(FIFO1, 0777) < 0) && (errno != EEXIST)) { printf("not can create\n"); } if((mkfifo(FIFO2, 0777) < 0) && (errno != EEXIST)) { printf("not can create\n"); } //使用管道需要close关闭成单通道 //使用fifo则需要使用open结合IO设为只读或只写 //client线程对FIFO1管道进行写操作 writeFd = open(FIFO1, O_WRONLY, 0);//如果这两个open调换则会出现死锁情况都在等待对应的open写操作 //client线程对FIFO2管道进行读操作 readFd = open(FIFO2, O_RDONLY, 0); //操作时基本一致,都要注意进程的执行顺序,防止阻塞成为死锁 //sleep(10); write(writeFd, "2.hello\n", strlen("2.hello\n")); sleep(10); memset(buf, 0, sizeof(buf)); read(readFd, buf, 1024); printf("%s", buf); close(writeFd); close(readFd); sleep(10); //最后 管道在进程结束会自动回收,但是fifo需要手动unlink去回收创建fifo时创建的文件 //注意,最后删除FIFO的都是客户端,因为操作fifo的是客户 unlink(FIFO1);//删除FIFO建立的文件 unlink(FIFO2); return 0;}
//server进程#include 
#include
#include
#include
#include
#include
#include
#include
#include "unpipc.h"#include
int main(){ int readFd = 0; int writeFd = 0; char buf[1024] = { 0}; //mkfifo创建有名管道,0777表示管道文件的权限, if((mkfifo(FIFO1, 0777) < 0) && (errno != EEXIST)) { printf("not can create\n"); } if((mkfifo(FIFO2, 0777) < 0) && (errno != EEXIST)) { printf("not can create\n"); } sleep(2); //server线程对FIFO1管道进行读操作, readFd = open(FIFO1, O_RDONLY, 0);//会堵塞在这里直到有open的写操作 //server线程对FIFO2管道进行写操作 writeFd = open(FIFO2, O_WRONLY, 0); sleep(3); memset(buf, 0, sizeof(buf)); read(readFd, buf, 10); printf("%s", buf); write(writeFd, "1.hello\n", strlen("1.hello\n")); sleep(10); close(writeFd); close(readFd); //注意,最后删除FIFO的都是客户端,因为操作fifo的是客户 //unlink(FIFO1); //unlink(FIFO2); return 0;}

7.7.2、管道和FIFO设置额外非阻塞属性

非阻塞属性 O_NONBLOCK

首先在open的时候可以指定

writeFd = open(FIFO1, O_WRONLY | O_NONBLOCK, 0);

但是管道没有open或者fifoopen之后还有需求需要更改则需要fcntl

注意使用fcntl设置属性,需要写读取,在或新增,再设置进入不然会冲掉之前设置的属性。

int flags;if( (flags = fcntl(fd, F_GETFL, 0)) < 0){
//属性获取失败}flags |= O_NONBLOCK;if( (flags = fcntl(fd, F_SETFL, 0)) < 0){
//属性设置失败}

7.7.3、FIFO用于单服务器多客服端实战

FIFO真正的优势在于服务器可以是一个长期存在的进程(如守护进程),而且可以与客户进程毫无关系。

服务器进程可以使用一个总所周知的名字创建FIFO,并打开来读,而其他客服端也可以通过这个总所周知的名字的FIFO来写,从而抵达到服务器。
至于服务器如何向客户端建立fifo发送消息了,这个可以需要客服端向服务器发送消息中添加上一个自己独有的信息,并客户端自己根据这个信息创建独有的fifo文件以及通道,并打开读写,直到服务器也去以写模式打开这个独有信息的fifo向其发送消息。从而达到服务器向客户端传递。

注意;服务器端打开服务端FIFO的时候要先以读打开,并且也要以写打开,虽然写操作不用,但是为了保证读操作一直长期存在而不用每次都去以读open,因为对于从空管道或者空FIFO read的时候,堵塞情况下,他会堵塞到管道中有数据或者他不再为写打开的时候为止,因此为了保证管道为空的时候他仍然阻塞,则需要特意为写打开这一操作

//服务端#include 
#include
#include
#include
#include
#include
#include
#include
#include "unpipc.h"#include
int main(){ int readFd = 0; int writeFd = 0; char buf[1024] = { 0}; //mkfifo创建有名管道,0777表示管道文件的权限, if((mkfifo(FIFOSEVER, 0777) < 0) && (errno != EEXIST)) { printf("not can create\n"); } //server线程对FIFO1管道进行读操作 readFd = open(FIFOSEVER, O_RDONLY, 0);//会堵塞在这里直到有open的写操作 //注意这里也要打开写,但是不会使用它,因为阻塞原因,可以减少每次连接都要再次以读打开而长期存在 writeFd = open(FIFOSEVER, O_WRONLY, 0); //这里循环处理即可处理单服务器多客服端的情况 //读到客户端写来的消息 memset(buf, 0, sizeof(buf)); read(readFd, buf, 200); printf("%s\n", buf); writeFd = open(buf, O_WRONLY, 0); write(writeFd, "1.hello\n", strlen("1.hello\n")); sleep(10); close(writeFd); close(readFd); //注意,最后删除FIFO的都是客户端,因为操作fifo的是客户 //unlink(FIFO1); //unlink(FIFO2); return 0;}
//客户端#include 
#include
#include
#include
#include
#include
#include
#include
#include "unpipc.h"#include
int main(){ int readFd = 0; int writeFd = 0; char fifoname[1024] = { 0}; char buf[1024] = { 0}; int pid_id = 0; pid_id = getpid();//获取当前进程ID sprintf(fifoname, "/tmp/fifo.%ld", (long)pid_id); //对FIFOSEVER的fifo以写模式打开 writeFd = open(FIFOSEVER, O_WRONLY, 0); write(writeFd, fifoname, sizeof(fifoname));//写入客服端这边独有的fifo名称 //创建客服端这边独有的fifo if((mkfifo(fifoname, 0777) < 0) && (errno != EEXIST)) { printf("not can create\n"); } //堵塞在这里,对客服端这边独有的fifo进行读操作 //等到服务器那边以写open打开该fifo时往下执行 readFd = open(fifoname, O_RDONLY, 0); memset(buf, 0, sizeof(buf)); read(readFd, buf, 1024); printf("%s", buf); close(writeFd); close(readFd); unlink(fifoname);//删除FIFO建立的文件 return 0;}

7.7.4、管道和FIFO的限制 OPEN_MAX, PIPE_BUF

OPEN_MAX 一个进程在任意时候打开的最大描述符

PIPE_BUF 可原子的往一个管道写入的最大数据量

7.7.5、管道和FIFO总结

管道 普通用于shell中但是在程序中也可以用于父子进程的回传消息,使用管道涉及这些API(pipe,fork,close,exec,waitpid)或者使用popen和pclose由他们处理并激活使用一个shell。

FIFO、使用mkfifo创建,open打开使用。但是open打开管道的时候必须小心,因为有许多规则制约着open是否堵塞。如单服务器多客户端那里需要将写open也打开,从而在空fifo时也进行堵塞

8、线程全解

8.1、线程引入的意义

意义就是完成对进程的改造,因为多进程之间的切换太耗失效率了。因此引入了轻量级的进程就是线程了。线程的改进就是在多线程切换和多线程通信上大大提升了效率。

线程是参与内核调度的最小单元

但是线程是依附于进程的,进程结束那么其进程内的线程也就不都结束了

一个进程可以fork产生其他进程,在进程中可以使用pthread_creat创建线程。

多进程通信就是多个可执行程序之间的通信,而多线程通信确实在同一个可执行程序中的不同函数中进行通信。并且也是被cpu统一调度的。

8.2、线程常见函数

1、线程创建与回收

(1)pthread_create		主线程用来创造子线程的NAME       pthread_create - create a new threadSYNOPSIS       #include 
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);//thread 线程号,attr线程属性一般设置为NULL,start_routine线程绑定的执行函数,arg传入参数 Compile and link with -pthread.//用法如下、 void *func(void *arg) {
} pthread_t th = -1; ret = pthread_create(&th, NULL, func, NULL); if (ret != 0) {
printf("pthread_create error.\n"); return -1; }
(2)pthread_join			主线程用来等待(阻塞)回收子线程,回收子线程本身的资源(3)pthread_detach		主线程用来分离子线程,分离后主线程不必再去回收子线程注意回收和分离必须选择一个NAME       pthread_join - join with a terminated threadSYNOPSIS       #include 
int pthread_join(pthread_t thread, void **retval);//thread 要回收的线程号 retval 回收线程的状态 Compile and link with -pthread.NAME pthread_detach - detach a threadSYNOPSIS #include
int pthread_detach(pthread_t thread);

2、线程取消

(1)pthread_cancel		一般都是主线程调用该函数去取消(让它赶紧死)子线程(2)pthread_setcancelstate	子线程设置自己是否允许被取消(3)pthread_setcanceltype   取消状态(前提是子线程先设置为允许被取消),				两种状态;立即死还是等待可以被杀死的时候再死(如持有互斥锁时不能死)

3、线程函数退出相关

(1)pthread_exit与return退出pthread_exit退出并会返回一个状态给主线程的回收函数jion,注意不能使用exit退出,否则整个进程都退出。(2)pthread_cleanup_push(3)pthread_cleanup_pop 线程可以安排他退出时需要调用的函数,这与进程可以用atexit函数安排进程退出时需要调用的函数是类似的。这样的函数称为线程清理处理程序,线程可以建立多个清理处理程序。处理程序记录在栈中,也就是说他们的执行顺序与他们注册的顺序相反。      头文件:#include 
函数原型:void pthread_cleanup_push(void (*rtn)(void *), void *arg); void pthread_clean_pop(int execute); void(*rtn)(void *): 线程清理函数int cnt = 0;类似于信号量if(cnt == 0)当cnt = 0就是没有锁当该线程可以进入进行操作,反正不能进入进行操作{
cnt++;//上锁,则其他线程无法进入,及时获得cpu调度也无法进入了 pthread_cleanup_push(fuction,null)//将函数fuction压人清理函数的栈中,则当子线程被中途杀死回收则会再执行以下清理函数栈中的函数,做最后清理工作,防止影响其他线程//执行子线程的操作,就是因为在执行的时候可能被主线程立即cancel掉//则此时cnt=1,上锁了,其他线程进不来,则会导致整个程序混乱,而cleanup就是解决这个问题的,被立即cancel掉之后还去执行清理栈中的函数去将cnt还原 pthread_clean_pop(0);cnt--;}void fuction(void *arg){
cnt--;}

4、获取线程id

(1)pthread_selfpthread_t pthread_self(void);函数作用:获得线程自身的ID。pthread_t的类型为unsigned long int,所以在打印的时候要使用%lu方式,否则显示结果出问题。

8.3、线程同步之信号量

信号量就可以实现子线程堵塞,通过主线程来唤醒的。

信号量的本质就是之前那个int cnt数一样的,通过修改cnt来阻塞子线程,主线程修改从而激活子线程

int sem_init(sem_t *sem, int pshared, unsigned int value);返回值 0则成功,错误时,返回 -1,并把 errno 设置为合适的值sem 创建这个信号量,为输出型,一般定义一个sem_t变量,再传入&sem_tpshared一般传入0;指明信号量的类型。不为0时此信号量在进程间共享,否则只能为当前进程的所有线程共享。value信号的初始值,一般为0int sem_destroy(sem_t *sem);,其中sem是要销毁的信号量。只有用sem_init初始化的信号量才能用sem_destroy销毁。int sem_wait(sem_t *sem);等待信号量,如果信号量的值大于0,将信号量的值减1,立即返回。如果信号量的值为0,则线程阻塞。相当于P操作。成功返回0,失败返回-1。int sem_post(sem_t *sem); 释放信号量,让信号量的值加1。相当于V操作。

案例;

主线程获取用户以终端输入任意个字符,并判断为end结束退出计算,而子线程进行计算。

#include 
#include
#include
#include
#include
char buf[200] = {
0};sem_t sem;unsigned int flag = 0;//子线程void *func(void *arg){
//开启子线程后应该阻塞,等待主线程那边开始接收字符后再启动 sem_wait(&sem); while(flag == 0) {
printf("本次输入了 %ld 个字符\n", strlen(buf)); memset(buf, 0, sizeof(buf)); sem_wait(&sem); } //pthread_exit(NULL);//线程退出 return 0;}int main(){
//主线程完成接收输入, //子线程负责计算输出 int ret = -1; pthread_t th = -1; //注册信号 sem_init(&sem, 0, 0); ret = pthread_create(&th, NULL, func, NULL); if (ret != 0) {
printf("pthread_create error.\n"); exit(-1); } //主线程 printf("输入一个字符串,以回车结束\n"); while(~scanf("%s", buf)) {
if(!strcmp(buf , "end"))//结束 {
flag = 1; printf("程序结束\n"); //因为子线程还阻塞在sem_wait(&sem);需要激活才能判断flag退出去 sem_post(&sem); break; } //开始接收后sem_post激活信号,子线程开始进行计算 sem_post(&sem); } // 最后还要回收子线程和销毁信号 printf("等待回收子线程\n"); ret = pthread_join(th, NULL); if (ret != 0) {
printf("pthread_join error.\n"); exit(-1); } printf("子线程回收成功\n"); sem_destroy(&sem); return 0;}

8.3、线程同步之互斥锁

相关函数:

pthread_mutex_init
pthread_mutex_destroy
pthread_mutex_lock
pthread_mutex_unlock
互斥锁和信号量的关系:可以认为互斥锁是一种特殊的信号量,只有0,1两个值的信号量,
互斥锁主要用来实现关键段保护,互斥,使用前上锁保证其他线程不会进来执行,执行完成后解锁,让其他线程执行。但是要注意上锁解锁的顺序,不能形成死锁,都在等待解锁。

#include 
#include
#include
#include
#include
#include
char buf[200] = { 0};unsigned int flag = 0;pthread_mutex_t mutex;//子线程void *func(void *arg){ //开启子线程后应该阻塞,等待主线程那边开始接收字符后再启动 sleep(1); //等主线程那边先上锁,这边才往下执行阻塞在上锁那里,不然这边先被调度则无法接收了 while(flag == 0) { pthread_mutex_lock(&mutex); printf("本次输入了 %ld 个字符\n", strlen(buf)); memset(buf, 0, sizeof(buf)); pthread_mutex_unlock(&mutex); sleep(1); //解锁后应该等待一会。让主线程被调度,让他上锁,否则子线程这边又上锁了,则无法接收输入了。 } //pthread_exit(NULL);//线程退出 return 0;}int main(){ //主线程完成接收输入, //子线程负责计算输出 int ret = -1; pthread_t th = -1; pthread_mutex_init(&mutex, NULL);//创建互斥锁 ret = pthread_create(&th, NULL, func, NULL); if (ret != 0) { printf("pthread_create error.\n"); exit(-1); } //主线程 printf("输入一个字符串,以回车结束\n"); while(1) { pthread_mutex_lock(&mutex); scanf("%s", buf); pthread_mutex_unlock(&mutex); if(!strcmp(buf , "end"))//结束 { flag = 1; printf("程序结束\n"); break; } //要等待一会,让子线程得到调度进行上锁操作,不然主线程这边又上锁了 sleep(1); } // 最后还要回收子线程和销毁信号 printf("等待回收子线程\n"); ret = pthread_join(th, NULL); if (ret != 0) { printf("pthread_join error.\n"); exit(-1); } printf("子线程回收成功\n"); pthread_mutex_destroy(&mutex); return 0;}

8.3、线程同步之条件变量

什么是条件变量;类似于进程中的异步IO,等待条件阻塞,另一个线程满足条件则唤醒等待的进程

相关函数		pthread_cond_init	创建		pthread_cond_destroy  销毁		pthread_cond_wait		等待;注意在等待的时候需要与互斥锁搭配,进行上锁。		pthread_cond_signal/pthread_cond_broadcast  唤醒,		pthread_cond_signal只能发送给其中的一个线程		pthread_cond_broadcast广播多路都唤醒
#include 
#include
#include
#include
#include
#include
char buf[200] = { 0};unsigned int flag = 0;pthread_mutex_t mutex;pthread_cond_t cond;//子线程void *func(void *arg){ //开启子线程后应该阻塞,等待主线程那边开始接收字符后再启动 while(flag == 0) { pthread_mutex_lock(&mutex);//保证只有一个子线程卡在这里面等待条件变量 pthread_cond_wait(&cond, &mutex); printf("本次输入了%ld个字符\n", strlen(buf)); memset(buf, 0, sizeof(buf)); pthread_mutex_unlock(&mutex); } //pthread_exit(NULL);//线程退出 return 0;}int main(){ //主线程完成接收输入, //子线程负责计算输出 int ret = -1; pthread_t th = -1; pthread_mutex_init(&mutex, NULL);//创建互斥锁 pthread_cond_init(&cond, NULL);//创建条件变量 ret = pthread_create(&th, NULL, func, NULL); if (ret != 0) { printf("pthread_create error.\n"); exit(-1); } //主线程 printf("输入一个字符串,以回车结束\n"); while(1) { scanf("%s", buf); pthread_cond_signal(&cond);//发送条件变量满足信号 if(!strcmp(buf , "end"))//结束 { flag = 1; printf("程序结束\n"); break; } } // 最后还要回收子线程和销毁信号 printf("等待回收子线程\n"); ret = pthread_join(th, NULL); if (ret != 0) { printf("pthread_join error.\n"); exit(-1); } printf("子线程回收成功\n"); pthread_mutex_destroy(&mutex); pthread_cond_destroy(&cond); return 0;}

转载地址:https://blog.csdn.net/zw1996/article/details/113994362 如侵犯您的版权,请留言回复原文章的地址,我们会给您删除此文章,给您带来不便请您谅解!

上一篇:linux之网络通信
下一篇:linux之获取系统信息

发表评论

最新留言

哈哈,博客排版真的漂亮呢~
[***.90.31.176]2024年04月17日 17时59分37秒