【Linux】进程间通信 -> 匿名管道命名管道
进程间通信的目的
- 数据传输:一个进程许需要将它的数据发送给另外一个进程。
- 资源共享:多个进程之间共享同样的资源。
- 通知事件:一个进程需要向另一个或一组进程发送消息,通知它们发生了某种事件(如进程终止时要通知父进程)。
- 进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。
进程间通信的分类
- 管道
- 匿名管道pipe
- 命名管道FIFO
- Sytem V IPC
- System V消息队列
- System V共享内存
- System V信号量
- POSIX IPC
- 消息队列
- 共享内存
- 信号量
- 互斥量
- 条件变量
- 读写锁
管道
我们把一个进程连接到另一个进程的一个数据流,称为管道。
进程是具有独立性的!要让进程间进行通信,“成本”一定不低。要让不同进程通信,首先要先让它们看到同一份资源。其次是通信。
这个公共的资源是谁提供的呢?其中一个进程?直接在进程内部创建资源,其他进程看不到。
所以,我们该如何理解进程间通信的本质问题呢?
- OS需要给直接或间接给通信双方进程提供“内存空间”
- 要通信的进程,必须看到同一份资源!
所谓不同的通信种类,本质就是:上面所说的资源,是OS中的哪一个模块提供的!
未来学习的进程间通信的接口,与其说是通信的接口,不如说它是让不同的进程看到同一份资源的接口。
匿名管道
如果是一个普通文件,需要将内核缓冲区里的数据刷新到磁盘中。但是进程间通信,是一个进程的数据给另外一个进程,是内存到内存之间的。不需要将内核缓冲区里的数据刷新到磁盘,另一个进程再从磁盘中读取,因为会大大降低通信的效率。
既然不需要刷新缓冲区,那么OS就不需要在磁盘中创建打开文件,然后在内存中创建struct file对象。OS不需要访问磁盘,直接就可以在内存中创建struct file对象,创建对应的缓冲区,然后将对象的地址填入到文件描述符中,那么再fork创建子进程时,子进程会拷贝父进程的文件描述符表,通过文件描述符,进而父子进程就能看到同一个文件。父子进程双方就能基于这个内存级文件来进行通信了。
一般在文件里面标定一个文件使用的是文件名,但是这个管道文件,是一个内存级文件,没有名字所以叫匿名管道。
从文件描述符角度理解管道
- 为什么让父进程分别以读和写的方式打开同一文件呢?
如果以只读或只写方式打开文件,那么子进程也会继承父进程的只读或只写方式,父子进程双方打开文件的方式是一样的,就完不成单向通信了。只有分别以读和写的方式打开,读和写的文件描述符才会被子进程继承,然后再选择对应的通信方向,关闭特定的文件描述符即可。
以读和写方式打开文件的本质:就是让子进程也能看到读写段端,让后续能自由的选择通信方向。
- 必须要关闭父子进程特定的文件描述符吗?例如父进程写,关闭读端,子进程读,关闭写端。
也可以不关特定的文件描述符。但是一般都建议关掉,因为这个不用的文件描述符有可能被别人用到,进而就有可能修改管道数据,引起程序运行出问题。
再来理解管道:父进程通过调用管道特定的系统调用,以读和写的方式打开一个内存级文件,再通过fork创建子进程的方式,被子进程继承下去,再关闭对应的读写端,进而形成的一条通信信道,这一套通信信道是基于文件的,所以叫管道。
用fork来共享管道原理
从内核角度理解管道本质
看待管道,就如同看待文件一样,管道的使用和文件一致,迎合了“Linux下一切皆文件”的思想。
创建匿名管道
参数fd :文件描述符数组, 其中 fd[0] 表示读端, fd[1] 表示写端。返回值: 成功返回 0 ,失败返回-1。文件描述符fd[0]、fd[1]默认从3开始,因为fd0、1、2默认被三个标准输入输出占用。
#include <iostream>
#include <cstring>
#include <sys/types.h>
#include <unistd.h>
#include <cassert>
#include <sys/types.h>
#include <sys/wait.h>
using namespace std;int main()
{// 第一步:创建管道文件,打开读写端int fds[2];int n = pipe(fds); // 成功返回0,失败返回-1assert(n == 0);// 第二步:创建子进程pid_t id = fork();assert(id >= 0);if (id == 0){// 子进程进行写入close(fds[0]);// 子进程的通信代码int cnt = 0;const char *s = "我是子进程,我正在给你发消息";while (true){cnt++;char buffer[1024]; // 只有子进程能看到!snprintf(buffer, sizeof buffer, "child->parent say: %s[%d][%d]", s, cnt, getpid());write(fds[1], buffer, strlen(buffer));sleep(1); // 细节:子进程每隔一秒写一次}// 子进程close(fds[1]);exit(0);}// 父进程进行读取close(fds[1]);// 父进程的通信代码while (true){char buffer[1024];ssize_t s = read(fds[0], buffer, sizeof(buffer) - 1); // 多留一个位置给\0if (s > 0){buffer[s] = 0;cout << "Get Message# " << buffer << " | my pid: " << getpid() << endl;// 细节:父进程没有进行sleep}}n = waitpid(id, nullptr, 0);assert(n == id);close(fds[0]);
}
运行结果,父进程每隔一秒输出一次 。

- 如果将子进程休眠时间改为5秒,会有什么现象呢?
//子进程
sleep(5);
运行结果,父进程每隔5秒输出一次。
- 一开始子进程写入,父进程读取,输出。之后在子进程休眠的5秒内,父进程在干什么呢?
我们将代码改造一下:
// 父进程的通信代码while (true){char buffer[1024];cout<<"AAAAAAAAAAAAAA"<<endl;ssize_t s = read(fds[0], buffer, sizeof(buffer) - 1); cout<<"BBBBBBBBBBBBBB"<<endl;if (s > 0){buffer[s] = 0;cout << "Get Message# " << buffer << " | my pid: " << getpid() << endl;}}
可以看到父进程在read()这里阻塞了。
read这里就涉及了两个功能:
- 等缓冲区里有数据。
- 将数据从内核拷贝到用户层。
如果此时缓冲区里没有数据,父进程就会一直阻塞等待。OS将父进程从运行状态R改为阻塞状态S,放在等待队列中。等待的不就是文件吗(等管道文件里有数据)?所以文件里也一定存在类似等待队列这样的结构,将进程的PCB放入这个文件对应的等待队列中。当写了之后,缓冲区有数据了,OS识别到,再将进程的PCB从等待队列拿到运行队列,将进程状态由S改为R,就可以继续被调度了。
总结:如果管道中没有了数据,读端再读,默认会直接阻塞当前正在读取的进程!
- 管道是一个固定大小的缓冲区。如果反过来,缓冲区写满了之后,写端继续写呢?
将代码改造一下:
//子进程不休眠//... while (true){cnt++;char buffer[1024]; snprintf(buffer, sizeof buffer, "child->parent say: %s[%d][%d]", s, cnt, getpid());write(fds[1], buffer, strlen(buffer));cout << "count: " << cnt << endl;}//...//父进程休眠1000秒//...while (true){sleep(1000);char buffer[1024];ssize_t s = read(fds[0], buffer, sizeof(buffer) - 1); if (s > 0){buffer[s] = 0;cout << "Get Message# " << buffer << " | my pid: " << getpid() << endl;}}//...
一瞬间就写满了,不再继续写了。
可以看到,子进程一瞬间就将缓冲区写满了,不再继续写了。
总结:如果管道满了之后,写端再写,会发生阻塞等待,等待读端读取。
再将代码改造一下:
//子进程不休眠//... while (true){cnt++;char buffer[1024]; snprintf(buffer, sizeof buffer, "child->parent say: %s[%d][%d]", s, cnt, getpid());write(fds[1], buffer, strlen(buffer));cout << "count: " << cnt << endl;}//...//父进程休眠2秒//...while (true){sleep(2);char buffer[1024];ssize_t s = read(fds[0], buffer, sizeof(buffer) - 1); if (s > 0){buffer[s] = 0;cout << "Get Message# " << buffer << " | my pid: " << getpid() << endl;}}//...
总结:父进程读取并不是一行一行读取的,而是按照指定大小读取的,也就是说缓冲区里有指定字节大小的数据,一次就会全部读完。
ssize_t s = read(fds[0], buffer, sizeof(buffer) - 1);
- 如果子进程写了一次之后,就将对应的写端描述符关闭呢?
// ...// 子进程// ...while (true){cnt++;char buffer[1024]; snprintf(buffer, sizeof buffer, "child->parent say: %s[%d][%d]", s, cnt, getpid());cout << "count: " << cnt << endl;write(fds[1], buffer, strlen(buffer));break;}close(fds[1]);cout << "子进程关闭写端" << endl;exit(0);}// ...// 父进程// ...while (true){sleep(2);char buffer[1024];ssize_t s = read(fds[0], buffer, sizeof(buffer) - 1); if (s > 0){buffer[s] = 0;cout << "Get Message# " << buffer << " | my pid: " << getpid() << endl;}else if (s == 0){cout << "read: " << s << endl;break;}}n = waitpid(id, nullptr, 0);assert(n == id);close(fds[0]);
父进程将管道数据读完之后,写端文件描述符也关闭了,那么就意味着已经完成了管道的读写,读端read()读到文件末尾,返回0。
- 如果关闭读端,写端继续写呢?
// ...// 子进程// ...while (true){cnt++;char buffer[1024]; snprintf(buffer, sizeof buffer, "child->parent say: %s[%d][%d]", s, cnt, getpid());cout << "count: " << cnt << endl;write(fds[1], buffer, strlen(buffer));}close(fds[1]);cout << "子进程关闭写端" << endl;exit(0);}// ...// 父进程// ...while (true){sleep(2);char buffer[1024];ssize_t s = read(fds[0], buffer, sizeof(buffer) - 1); if (s > 0){buffer[s] = 0;cout << "Get Message# " << buffer << " | my pid: " << getpid() << endl;}else if (s == 0){cout << "read: " << s << endl;break;}break;}close(fds[0]);cout << "父进程关闭读端" << endl;int status = 0;n = waitpid(id, &status, 0);cout << "pid->" << n << " : " << (status & 0x7F) << endl;assert(n == id);
如果读端被关闭,写就没有意义了没有意义操作系统会杀掉写的子进程,是通过发送信号的方式被杀掉,也就相当于子进程异常退出了。一旦父进程关闭读端,子进程会立马退出,父进程waipid()就能获取到子进程的退出码。
OS会给子进程直接发送13号信号,来终止写进程。
读写特征
- 读快,写慢:读阻塞,等待写入。
- 读慢,写快:写阻塞,等待读取。
- 写关闭:读到0。
- 读关闭,终止写。
这四种读写特征分别对应了上述各种现象。
管道的特征
- 管道的生命周期随进程,进程退出,管道释放。
- 只能用于具有共同祖先(具有血缘关系的进程)之间进行通信,通常,一个管道由一个进程创建,然后该进程调用fork,此后,父子进程之间就可以应用管道。常用于父子通信。
- 管道是面向字节流的。
- 内核会对管道操作进行同步与互斥,对共享资源进行保护的方案。
- 管道是半双工的,数据只能向一个方向流动,需要双方进行通信时,需要建立两个管道。
命名管道
匿名管道应用的一个限制就是只能在具有血缘关系的进程之间进行通信,如果我们想在不相关的进程之间交换数据,可以使用FIFO文件来完成,被称为命名管道,命名管道是一种特殊类型的文件。
命名管道可以从命令行上创建:
$mkfifo filename
- 命名管道是如何让不同的进程看到同一份资源的呢?
命名管道也可以通过函数创建:
- 创建
成功返回0,失败返回-1。
- 删除
成功返回0,失败返回-1。
命名管道与匿名管道的区别
- 匿名管道由pipe函数创建并打开。
- 命名管道由mkfififo函数创建,打开用open
- FIFO(命名管道)与pipe(匿名管道)之间唯一的区别在它们创建与打开的方式不同,一但这些工作完 成之后,它们具有相同的语义。
匿名管道是通过文件描述符来让具有血缘关系的进程进行通信的。
命名管道是通过文件名来让不同的进程使用同一个管道通信的。
client&server通信
示例代码:
Makefile:
.PHONY:all
all:server clientserver:server.ccg++ -o $@ $^ -std=c++11 -gclient:client.ccg++ -o $@ $^ -std=c++11 -g.PHONY:clean
clean:rm -f server client
comm.hpp
#pragma once#include <iostream>
#include <string>
#include <cstring>
#include <cerrno>
#include <cassert>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>#define NAMED_PIPE "/tmp/mypipe"bool createFifo(const std::string &path)
{umask(0);int n = mkfifo(path.c_str(), 0600);if (n == 0)return true;else{std::cout << "errno: " << errno << " err string: " << strerror(errno) << std::endl;return false;}
}void removeFifo(const std::string &path)
{int n = unlink(path.c_str());assert(n == 0);//assert只在Debug下有效,在release下就没有了。(void)n;
}
server.cc
#include "comm.hpp"int main()
{bool r = createFifo(NAMED_PIPE);assert(r);(void)r;//removeFifo(NAMED_PIPE);return 0;
}
此时就在tmp路径下创建了名为“mypipe”的管道。
下面来进行server和client的通信。通信的过程就是对文件的读写读写操作。
client.cc
int main()
{int wfd = open(NAMED_PIPE,O_WRONLY);if(wfd<0) exit(1);char buffer[1024];while(true){std::cout<<"Please say# ";fgets(buffer,sizeof buffer,stdin);ssize_t s = write(wfd,buffer,strlen(buffer));assert(s==strlen(buffer));(void)s;}close(wfd);return 0;
}
server.cc
int main()
{bool r = createFifo(NAMED_PIPE);assert(r);(void)r;std::cout<<"server begin"<<std::endl;int rfd = open(NAMED_PIPE, O_RDONLY);std::cout<<"server end"<<std::endl;if(rfd < 0) exit(1);//readchar buffer[1024];while(true){ssize_t s = read(rfd, buffer, sizeof(buffer)-1);if(s > 0){buffer[s] = 0;std::cout << "client->server# " << buffer << std::endl;}//如果读端关闭,写端读到0else if(s == 0){std::cout << "client quit, me too!" << std::endl;break;}//读取错误else{std::cout << "err string: " << strerror(errno) << std::endl;break;}}close(rfd);// sleep(10);removeFifo(NAMED_PIPE);return 0;
}
这样就完成了客户端client和服务端server两个进程间的通信。
- 为什么服务端读取的时候会多一行空行呢?
原因是我们从键盘输入的时候会摁回车键,例如:输入“hello world”。实际fgets读取到的为:“hello world \n”。“\n”也会被读取到,所以会被多打印一行空行。
再对代码做一下优化。
client.cc:
int main()
{std::cout<<"client begin"<<std::endl;int wfd = open(NAMED_PIPE,O_WRONLY);std::cout<<"client end"<<std::endl;if(wfd<0) exit(1);char buffer[1024];while(true){std::cout<<"Please say# ";fgets(buffer,sizeof buffer,stdin);if(strlen(buffer)>0) buffer[strlen(buffer)-1] = 0;ssize_t s = write(wfd,buffer,strlen(buffer));assert(s==strlen(buffer));(void)s;}close(wfd);return 0;
}
server.cc:
int main()
{bool r = createFifo(NAMED_PIPE);assert(r);(void)r;std::cout<<"server begin"<<std::endl;int rfd = open(NAMED_PIPE, O_RDONLY);std::cout<<"server end"<<std::endl;if(rfd < 0) exit(1);//readchar buffer[1024];while(true){ssize_t s = read(rfd, buffer, sizeof(buffer)-1);if(s > 0){buffer[s] = 0;std::cout << "client->server# " << buffer << std::endl;}//如果读端关闭,写端读到0else if(s == 0){std::cout << "client quit, me too!" << std::endl;break;}//读取错误else{std::cout << "err string: " << strerror(errno) << std::endl;break;}}close(rfd);// sleep(10);removeFifo(NAMED_PIPE);return 0;
}
当读端先执行时,会在open()阻塞。
当写端再执行时,读端才继续调度执行。
总结:只有当两个进程同时打开文件程序才能继续向后运行。
相关文章:

【Linux】进程间通信 -> 匿名管道命名管道
进程间通信的目的 数据传输:一个进程许需要将它的数据发送给另外一个进程。资源共享:多个进程之间共享同样的资源。通知事件:一个进程需要向另一个或一组进程发送消息,通知它们发生了某种事件(如进程终止时要通知父进程…...
大数据开发学习路线
编程语言: Python:数据分析、数据预处理 Java:Hadoop和许多大数据工具的基础 Scala:用于Apache Spark数据库知识: SQL和NoSQL数据库的基本概念 数据库系统如MySQL、MongoDB等操作系统: Linux基础命令和脚本…...

华为云计算HCIE笔记05
第七章:其它模式 灾备组网 高可用性组网,单核心场景下,直接在两个站点中设置一个第三方仲裁站点,两个站点同时连接到仲裁,并且连接到对方。一旦出现问题,则由仲裁站点进行判断,进行业务切换 双核…...
wordpress网站用token登入开发过程
生成跳转token 示例: function generate_login_token($user_id, $secret_key) {$payload [user_id > $user_id,timestamp > time(),];$payload_json json_encode($payload);$signature hash_hmac(sha256, $payload_json, $secret_key);return base64_en…...
Python基础知识回顾
数据类型 Python可以区分整数(integers、下文简写为int)、浮点数(float)、字符串(string)和布尔值(Boolean)等数据类型。 1)int是可正可负的整数 2)float包…...

C++--------效率和表示
C 效率和表示 效率 时间效率:在 C 中,不同的数据结构和算法有着各异的时间复杂度。例如,访问数组元素的时间复杂度是 O ( 1 ) O(1) O(1),而遍历链表查找元素的时间复杂度最坏情况下是 O ( n ) O(n) O(n)。选择合适的算法与数据…...
在 Ubuntu 服务器上添加和删除用户
在 Ubuntu 服务器上添加和删除用户通常使用命令行工具,如 adduser、useradd、deluser 等。以下是详细的步骤和说明: 添加用户 使用 adduser 命令 adduser 是一个更为友好的脚本,用于创建新用户并设置相关信息。 添加新用户 sudo adduser 用…...

安卓 SystemServer 启动流程
目录 引言 Android系统服务启动顺序 zygote fork SystemServer 进程 SystemServer启动流程 1、SystemServer.main() 2、SystemServer.run() 3、初始化系统上下文 4、创建系统服务管理 5、启动系统各种服务 总结 引言 开机启动时 PowerManagerService 调用 AudioSer…...

深度分析 es multi_match 中most_fields、best_fields、cross_fields区别
文章目录 1. multi_match 查询的类型1.1 best_fields(默认)1.2 most_fields1.3 cross_fields 2. 不同类型的示例查询示例数据: 3. 示例 1: 使用 best_fields查询:说明: 4. 示例 2: 使用 most_fields查询:说…...

中职计算机网络技术理实一体化实训室建设方案
构建理实一体化教学模式对于改善中等职业学校计算机网络技术课程的教学现状、提升教学质量和效率具有重要意义。在中职教育不断深化改革的背景下,积极推进理实一体化教学模式的发展,不仅能够提高计算机网络技术课程的教学水平,满足教育改革的…...
Java技术专家视角解读:SQL优化与批处理在大数据处理中的应用及原理
引言 在大厂架构中,提升系统性能和稳定性是技术团队的首要任务。SQL优化与批处理作为两大关键技术手段,对于处理大规模数据和高并发请求具有重要意义。本文将从Java技术专家的视角出发,深入探讨SQL优化与批处理在大数据处理中的应用及原理&a…...

数据结构(Java版)第六期:LinkedList与链表(一)
目录 一、链表 1.1. 链表的概念及结构 1.2. 链表的实现 专栏:数据结构(Java版) 个人主页:手握风云 一、链表 1.1. 链表的概念及结构 链表是⼀种物理存储结构上⾮连续存储结构,数据元素的逻辑顺序是通过链表中的引⽤链接次序实现的。与火车…...

云边端一体化架构
云边端一体化架构是一种将云计算、边缘计算和终端设备相结合的分布式计算模型。该架构旨在通过优化资源分配和数据处理流程,提供更高效、更低延迟的服务体验。 下面是对这个架构的简要说明: 01云计算(Cloud Computing) — 作为中心…...

人工智能之基于阿里云进行人脸特征检测部署
人工智能之基于阿里云进行人脸特征检测部署 需求描述 基于阿里云搭建真人人脸68个关键点检测模型,模型名称:Damo_XR_Lab/cv_human_68-facial-landmark-detection使用上述模型进行人脸关键点识别,模型地址 业务实现 阿里云配置 阿里云配置…...

基于高云GW5AT-15 FPGA的SLVS-EC桥MIPI设计方案分享
作者:Hello,Panda 一、设计需求 设计一个4Lanes SLVS-EC桥接到2组4lanes MIPI DPHY接口的电路模块: (1)CMOS芯片:IMX537-AAMJ-C,输出4lanes SLVS-EC 4.752Gbps Lane速率; (2&…...

MPLS小实验:利用LDP动态建立LSP
正文共:1234 字 19 图,预估阅读时间:2 分钟 通过上个实验(MPLS小实验:静态建立LSP),我们了解到静态LSP不依靠标签分发协议,而是在报文经过的每一跳设备上(包括Ingress、T…...
C++ 面向对象编程
面向对象编程(Object-Oriented Programming, OOP)是C语言的一个重要特性,它允许开发者以更直观和模块化的方式来设计和构建程序。OOP的四个主要原则是:封装(Encapsulation)、继承(Inheritance&a…...

我的Serverless实战——引领云计算的下一个十年,附答案
(Serverless模式下,按照实际消耗资源及使用存储进行计费) 4.更少的代码,更快的交付速度。 (Serverless提供成熟的代码构建发布、版本切换等特性,交付速度更快) Serverless由开发者实现的服务端逻…...
有哪些其他方法可以实现数据一致性验证?
数据库约束 主键约束: 主键是表中用于唯一标识每条记录的一列或一组列。例如,在一个“用户表”中,用户ID可以作为主键。当插入或更新数据时,数据库会自动检查主键值是否唯一。如果试图插入一个已存在主键值的记录,数据…...

vue 基础学习
一、ref 和reactive 区别 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><title>Title</title> </head> <body><div id"app"><h1>{{Web.title}}</h1><h1&…...

Unity3D中Gfx.WaitForPresent优化方案
前言 在Unity中,Gfx.WaitForPresent占用CPU过高通常表示主线程在等待GPU完成渲染(即CPU被阻塞),这表明存在GPU瓶颈或垂直同步/帧率设置问题。以下是系统的优化方案: 对惹,这里有一个游戏开发交流小组&…...

全球首个30米分辨率湿地数据集(2000—2022)
数据简介 今天我们分享的数据是全球30米分辨率湿地数据集,包含8种湿地亚类,该数据以0.5X0.5的瓦片存储,我们整理了所有属于中国的瓦片名称与其对应省份,方便大家研究使用。 该数据集作为全球首个30米分辨率、覆盖2000–2022年时间…...

【项目实战】通过多模态+LangGraph实现PPT生成助手
PPT自动生成系统 基于LangGraph的PPT自动生成系统,可以将Markdown文档自动转换为PPT演示文稿。 功能特点 Markdown解析:自动解析Markdown文档结构PPT模板分析:分析PPT模板的布局和风格智能布局决策:匹配内容与合适的PPT布局自动…...
Java 二维码
Java 二维码 **技术:**谷歌 ZXing 实现 首先添加依赖 <!-- 二维码依赖 --><dependency><groupId>com.google.zxing</groupId><artifactId>core</artifactId><version>3.5.1</version></dependency><de…...

html css js网页制作成品——HTML+CSS榴莲商城网页设计(4页)附源码
目录 一、👨🎓网站题目 二、✍️网站描述 三、📚网站介绍 四、🌐网站效果 五、🪓 代码实现 🧱HTML 六、🥇 如何让学习不再盲目 七、🎁更多干货 一、👨…...
Java + Spring Boot + Mybatis 实现批量插入
在 Java 中使用 Spring Boot 和 MyBatis 实现批量插入可以通过以下步骤完成。这里提供两种常用方法:使用 MyBatis 的 <foreach> 标签和批处理模式(ExecutorType.BATCH)。 方法一:使用 XML 的 <foreach> 标签ÿ…...
苹果AI眼镜:从“工具”到“社交姿态”的范式革命——重新定义AI交互入口的未来机会
在2025年的AI硬件浪潮中,苹果AI眼镜(Apple Glasses)正在引发一场关于“人机交互形态”的深度思考。它并非简单地替代AirPods或Apple Watch,而是开辟了一个全新的、日常可接受的AI入口。其核心价值不在于功能的堆叠,而在于如何通过形态设计打破社交壁垒,成为用户“全天佩戴…...

【堆垛策略】设计方法
堆垛策略的设计是积木堆叠系统的核心,直接影响堆叠的稳定性、效率和容错能力。以下是分层次的堆垛策略设计方法,涵盖基础规则、优化算法和容错机制: 1. 基础堆垛规则 (1) 物理稳定性优先 重心原则: 大尺寸/重量积木在下…...

前端开发者常用网站
Can I use网站:一个查询网页技术兼容性的网站 一个查询网页技术兼容性的网站Can I use:Can I use... Support tables for HTML5, CSS3, etc (查询浏览器对HTML5的支持情况) 权威网站:MDN JavaScript权威网站:JavaScript | MDN...

何谓AI编程【02】AI编程官网以优雅草星云智控为例建设实践-完善顶部-建立各项子页-调整排版-优雅草卓伊凡
何谓AI编程【02】AI编程官网以优雅草星云智控为例建设实践-完善顶部-建立各项子页-调整排版-优雅草卓伊凡 背景 我们以建设星云智控官网来做AI编程实践,很多人以为AI已经强大到不需要程序员了,其实不是,AI更加需要程序员,普通人…...