목차
发现问题
测试
如何解决
php教程 php手册 解决进程间共享内存,由于某个进程异常退出导致死锁问题

解决进程间共享内存,由于某个进程异常退出导致死锁问题

Jun 06, 2016 pm 08:10 PM
공유됨 메모리 ~로 이어지다 이상 이중 자물쇠 해결하다 프로세스 그만두다

发现问题 继这篇Blog 解决Nginx和Fpm-Php等内部多进程之间共享数据问题 发完后,进程间共享内存又遇到了新的问题 昨天晚上QP同学上线后,早上看超时报表发现有一台前端机器访问QP超时,比其他前端机器高出了几个数量级,前端的机器都是同构的 难道是这台机器

发现问题

继这篇Blog 解决Nginx和Fpm-Php等内部多进程之间共享数据问题 发完后,进程间共享内存又遇到了新的问题

昨天晚上QP同学上线后,早上看超时报表发现有一台前端机器访问QP超时,比其他前端机器高出了几个数量级,前端的机器都是同构的

难道是这台机器系统不正常?查看系统状态也没有任何异常,统计了一下超时日志,发现超时都发生在早上QP服务重启的过程中,正常情况下服务重启时,ClusterMap 会保证流量的正常分配

难道是ClusterMap有问题?去ClusterMap Server端看了一下,一切正常

难道是订阅者客户端有问题吗?随便找了一台正常的机器和有问题的这台机器对比,查看下日志也没有发现问题,使用查询工具检查这两台机器订阅者代理写的共享内存,发现工具读取共享内存返回的结果不一致,这就更奇怪了,都是相同的订阅者,一台机器有问题一台没问题

难道Server端给他们的消息不一致?去Server端把订阅者的机器列表都打了出来,发现了有问题的机器根本不在订阅者列表里面,说明这台机器没有订阅,貌似有点线索了,我下线了一台它订阅的QP机器验证,发现共享内部数据没有更新,pstack一下这个进程,发现内部的更新线程一直在等锁,导致共享内存数据一直无法更新,gdb跟进去之后,_lock.data.nr_readers一直为1,说明一直有一个读进程占着锁导致写进程无法进入,遍历了所有fpm-php的读进程发现都没有占着锁,这说明在读进程在获得锁后没来得及释放就挂掉了

测试

现在问题已经确认就是获得读锁后进程异常退出导致的,我写个测试程序复现这个问题


(! 2293)-> cat test/read_shared.cpp

#include
SharedUpdateData*   _sharedUpdateData = NULL;
cm_sub::CMMapFile*  _mmapFile = NULL;
int32_t initSharedMemRead(const std::string& mmap_file_path)
{
    _mmapFile = new (std::nothrow) cm_sub::CMMapFile();
    if (_mmapFile == NULL || !_mmapFile->open(mmap_file_path.c_str(), FILE_OPEN_WRITE) )
    {
        return -1;
    }
    _sharedUpdateData = (SharedUpdateData*)_mmapFile->offset2Addr(0);
    return 0;
}
int main(int argc, char** argv)
{
    if (initSharedMemRead(argv[1]) != 0) return -1;
    int cnt = 100;
    while (cnt > 0)
    {
        pthread_rwlock_rdlock( &(_sharedUpdateData->_lock));
        fprintf(stdout, "version = %ld, readers = %u\n",
            _sharedUpdateData->_version, _sharedUpdateData->_lock.__data.__nr_readers);
        if (cnt == 190)
        {  
            exit(0);
        }  
        sleep(1);
        pthread_rwlock_unlock( &(_sharedUpdateData->_lock));
        -- cnt;
        usleep(100*1000);
    }
    delete _mmapFile;
}
로그인 후 복사

(! 2293)-> cat test/write_shared.cpp

#include
SharedUpdateData*   _sharedUpdateData = NULL;
cm_sub::CMMapFile*  _mmapFile = NULL;
int32_t initSharedMemWrite(const char* mmap_file_path)
{
    _mmapFile = new (std::nothrow) cm_sub::CMMapFile();
    if ( _mmapFile == NULL || !_mmapFile->open(mmap_file_path, FILE_OPEN_WRITE, 1024) )
    {
        return -1;
    }
    _sharedUpdateData = (SharedUpdateData *)_mmapFile->offset2Addr(0);
    madvise(_sharedUpdateData, 1024, MADV_SEQUENTIAL);
    pthread_rwlockattr_t attr;
    memset(&attr, 0x0, sizeof(pthread_rwlockattr_t));
    if (pthread_rwlockattr_init(&attr) != 0 || pthread_rwlockattr_setpshared(&attr, PTHREAD_PROCESS_SHARED) != 0)
    {
        return -1;
    }
    pthread_rwlock_init( &(_sharedUpdateData->_lock), &attr);
    _sharedUpdateData->_updateTime = autil::TimeUtility::currentTime();
    _sharedUpdateData->_version = 0; 
    return 0;
}
int main()
{
    if (initSharedMemWrite("data.mmap") != 0) return -1;
    int cnt = 200;
    while (cnt > 0)
    {
        pthread_rwlock_wrlock( &(_sharedUpdateData->_lock));
        ++ _sharedUpdateData->_version;
        fprintf(stdout, "version = %ld, readers = %u\n",
                _sharedUpdateData->_version, _sharedUpdateData->_lock.__data.__nr_readers);
        sleep(1);
        pthread_rwlock_unlock( &(_sharedUpdateData->_lock));
        -- cnt;
        usleep(100*1000);
    }
    delete _mmapFile;
}
로그인 후 복사

无论是读进程还是写进程,获取锁后来不及释放就挂掉都会有这样的问题

如何解决

问题已经复现,想想如何用一个好的办法解决,在网上找了一遍,针对读写锁没有什么好的解决办法,只能在逻辑上自己解决,能想到的是使用超时机制,即写进程内部增加一个超时时间,如果写进程到了这个时间还是不能获得锁,就认为死锁,将读进程的计数减1,这是一个暴力的解决办法,不解释了,如果谁有好的解决办法指导我下

看下读写锁的代码,读写锁和互斥锁相比,更适合用在读多写少的场景,如果读进程需要锁住时间久,就更合适使用读写锁了,我的应该场景是,读多写少,读写时间都非常短;暂时认为互斥锁和读写锁性能差别应该不大,其实读写锁内部同样使用了互斥锁,只不过是锁的时间比较短,锁住互斥区,进去看下是否有人正在写,然后就释放了,
需要注意的是,读写锁默认是写优先的,也就是说当正在写,或者进入写队列准备写时,读锁都是加不上的,需要等待

好,那我们看看互斥锁能否解决我们的问题,互斥锁内部有一个属性叫Robust锁

设置锁为Robust锁: pthread_mutexattr_setrobust_np

        The robustness attribute defines the behavior when the owner
    of  a  mutex  dies.  The value of robustness could be either
    PTHREAD_MUTEX_ROBUST_NP or  PTHREAD_MUTEX_STALLED_NP,  which
    are  defined by the header . The default value of
    the robustness attribute is PTHREAD_MUTEX_STALLED_NP.
        When the owner of a mutex with the  PTHREAD_MUTEX_STALLED_NP
    robustness    attribute    dies,   all   future   calls   to
    pthread_mutex_lock(3C) for this mutex will be  blocked  from
    progress in an unspecified manner.
로그인 후 복사

修复非一致的Robust锁: pthread_mutex_consistent_np

        A consistent mutex becomes inconsistent and is  unlocked  if
    its  owner dies while holding it, or if the process contain-
    ing the owner of the mutex unmaps the memory containing  the
    mutex or performs one of the exec(2) functions. A subsequent
    owner  of  the   mutex   will   acquire   the   mutex   with
    pthread_mutex_lock(3C),  which  will  return  EOWNERDEAD  to
    indicate that the acquired mutex is inconsistent.
        The pthread_mutex_consistent_np() function should be  called
    while  holding  the  mutex  acquired  by  a previous call to
    pthread_mutex_lock() that returned EOWNERDEAD.
        Since the critical section protected by the mutex could have
    been  left  in  an inconsistent state by the dead owner, the
    caller should make the mutex consistent only if it  is  able
    to  make  the  critical  section protected by the mutex con-
    sistent.
로그인 후 복사

简单来说就是当发现EOWNERDEAD时,pthread_mutex_consistent_np函数内部会判断这个互斥锁是不是Robust锁,如果是,并且他OwnerDie了,那么他会把锁的owner设置成自己的进程ID,这样这个锁又可以恢复可用,很简单吧

锁释放是可以解决了,但是通过共享内存在进程间共享数据时,还有一点是需要注意的,就是数据的正确性,即完整性,进程共享不同与线程,如果是一个进程中的多个线程,那么进程异常退出了,其他线程也同时退出了,进程间共享都是独立的,如果一个写线程在写共享数据的过程中,异常退出,导致写入的数据不完整,读进程读取时就会有读到不完整数据的问题,其实数据完整性非常好解决,只需要在共享内存中加一个完成标记就好了,锁住共享区后,写数据,写好之后标记为完成,就可以了,读进程在读取时判断一下完成标记

测试代码见:


(! 2295)-> cat test/read_shared_mutex.cpp

 #include 
 SharedUpdateData*   _sharedUpdateData = NULL;
 cm_sub::CMMapFile*  _mmapFile = NULL;
 int32_t initSharedMemRead(const std::string& mmap_file_path)
 {
    _mmapFile = new (std::nothrow) cm_sub::CMMapFile();
    if (_mmapFile == NULL || !_mmapFile->open(mmap_file_path.c_str(), FILE_OPEN_WRITE) )
    {
        return -1;
    }
    _sharedUpdateData = (SharedUpdateData*)_mmapFile->offset2Addr(0);
    return 0;
 }
 int main(int argc, char** argv)
 {
     if (argc != 2) return -1;
     if (initSharedMemRead(argv[1]) != 0) return -1;   
     int cnt = 10000;
     int ret = 0;
     while (cnt > 0)
     {
         ret = pthread_mutex_lock( &(_sharedUpdateData->_lock));
         if (ret == EOWNERDEAD)
         {
             fprintf(stdout, "%s: version = %ld, lock = %d, %u, %d\n",
                strerror(ret),
                _sharedUpdateData->_version, 
                _sharedUpdateData->_lock.__data.__lock,
                _sharedUpdateData->_lock.__data.__count,
                _sharedUpdateData->_lock.__data.__owner);
             ret = pthread_mutex_consistent_np( &(_sharedUpdateData->_lock));
             if (ret != 0)
             {
                 fprintf(stderr, "%s\n", strerror(ret));
                 pthread_mutex_unlock( &(_sharedUpdateData->_lock));
                 continue;
             }
         }
         fprintf(stdout, "version = %ld, lock = %d, %u, %d\n", 
            _sharedUpdateData->_version, 
            _sharedUpdateData->_lock.__data.__lock,
            _sharedUpdateData->_lock.__data.__count,
            _sharedUpdateData->_lock.__data.__owner);
         sleep(5);
         pthread_mutex_unlock( &(_sharedUpdateData->_lock));
         usleep(500*1000);
         -- cnt;
    }
    fprintf(stdout, "go on\n");
    delete _mmapFile;
 }
로그인 후 복사

(! 2295)-> cat test/write_shared_mutex.cpp

#include 
SharedUpdateData*   _sharedUpdateData = NULL;
cm_sub::CMMapFile*  _mmapFile = NULL;
int32_t initSharedMemWrite(const char* mmap_file_path)
{
    _mmapFile = new (std::nothrow) cm_sub::CMMapFile();
    if ( _mmapFile == NULL || !_mmapFile->open(mmap_file_path, FILE_OPEN_WRITE, 1024) )
    {
        return -1;
    }
    _sharedUpdateData = (SharedUpdateData *)_mmapFile->offset2Addr(0);
    madvise(_sharedUpdateData, 1024, MADV_SEQUENTIAL);
    pthread_mutexattr_t attr;
    memset(&attr, 0x0, sizeof(pthread_mutexattr_t));
    if (pthread_mutexattr_init(&attr) != 0 || pthread_mutexattr_setpshared(&attr, PTHREAD_PROCESS_SHARED) != 0)
    {
        return -1;
    }
    if (pthread_mutexattr_setrobust_np(&attr, PTHREAD_MUTEX_ROBUST_NP) != 0)
    {
        return -1;
    }
    pthread_mutex_init( &(_sharedUpdateData->_lock), &attr);
    _sharedUpdateData->_version = 0;
    return 0;
}
int main()
{
    if (initSharedMemWrite("data.mmap") != 0) return -1;
    int cnt = 200;
    int ret = 0;
    while (cnt > 0)
    {
        ret = pthread_mutex_lock( &(_sharedUpdateData->_lock));
        if (ret == EOWNERDEAD)
        {
            fprintf(stdout, "%s: version = %ld, lock = %d, %u, %d\n",
                    strerror(ret),
                    _sharedUpdateData->_version,
                    _sharedUpdateData->_lock.__data.__lock,
                    _sharedUpdateData->_lock.__data.__count,
                                            _sharedUpdateData->_lock.__data.__owner);
            ret = pthread_mutex_consistent_np( &(_sharedUpdateData->_lock));
            if (ret != 0)
            {
                fprintf(stderr, "%s\n", strerror(ret));
                pthread_mutex_unlock( &(_sharedUpdateData->_lock));
                continue;
            }
        }
        ++ _sharedUpdateData->_version;
        fprintf(stdout, "version = %ld, lock = %d, %u, %d\n", _sharedUpdateData->_version,
                _sharedUpdateData->_lock.__data.__lock,
                _sharedUpdateData->_lock.__data.__count,
                _sharedUpdateData->_lock.__data.__owner);
        usleep(1000*1000);
        pthread_mutex_unlock( &(_sharedUpdateData->_lock));
        -- cnt;
        usleep(500*1000);
    }
    delete _mmapFile;
}
로그인 후 복사

BTW:我们都知道加锁是有开销的,不仅仅是互斥导致的等待开销,还有加锁过程都是有系统调用到内核态的,这个过程开销也很大,有一种互斥锁叫Futex锁(Fast User Mutex),Linux从2.5.7版本开始支持Futex,快速的用户层面的互斥锁,Fetux锁有更好的性能,是用户态和内核态混合使用的同步机制,如果没有锁竞争的时候,在用户态就可以判断返回,不需要系统调用,

当然任何锁都是有开销的,能不用尽量不用,使用双Buffer,释放链表,引用计数,都可以在一定程度上替代锁的使用

본 웹사이트의 성명
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.

핫 AI 도구

Undresser.AI Undress

Undresser.AI Undress

사실적인 누드 사진을 만들기 위한 AI 기반 앱

AI Clothes Remover

AI Clothes Remover

사진에서 옷을 제거하는 온라인 AI 도구입니다.

Undress AI Tool

Undress AI Tool

무료로 이미지를 벗다

Clothoff.io

Clothoff.io

AI 옷 제거제

Video Face Swap

Video Face Swap

완전히 무료인 AI 얼굴 교환 도구를 사용하여 모든 비디오의 얼굴을 쉽게 바꾸세요!

뜨거운 도구

메모장++7.3.1

메모장++7.3.1

사용하기 쉬운 무료 코드 편집기

SublimeText3 중국어 버전

SublimeText3 중국어 버전

중국어 버전, 사용하기 매우 쉽습니다.

스튜디오 13.0.1 보내기

스튜디오 13.0.1 보내기

강력한 PHP 통합 개발 환경

드림위버 CS6

드림위버 CS6

시각적 웹 개발 도구

SublimeText3 Mac 버전

SublimeText3 Mac 버전

신 수준의 코드 편집 소프트웨어(SublimeText3)

대용량 메모리 최적화, 컴퓨터가 16g/32g 메모리 속도로 업그레이드했는데 변화가 없다면 어떻게 해야 하나요? 대용량 메모리 최적화, 컴퓨터가 16g/32g 메모리 속도로 업그레이드했는데 변화가 없다면 어떻게 해야 하나요? Jun 18, 2024 pm 06:51 PM

기계식 하드 드라이브나 SATA 솔리드 스테이트 드라이브의 경우 소프트웨어 실행 속도의 증가를 느낄 수 있지만 NVME 하드 드라이브라면 느끼지 못할 수도 있습니다. 1. 레지스트리를 데스크탑으로 가져와 새 텍스트 문서를 생성하고, 다음 내용을 복사하여 붙여넣은 후 1.reg로 저장한 후 마우스 오른쪽 버튼을 클릭하여 병합하고 컴퓨터를 다시 시작합니다. WindowsRegistryEditorVersion5.00[HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\SessionManager\MemoryManagement]"DisablePagingExecutive"=d

소식통에 따르면 삼성전자와 SK하이닉스는 2026년 이후 적층형 모바일 메모리를 상용화할 것으로 보인다. 소식통에 따르면 삼성전자와 SK하이닉스는 2026년 이후 적층형 모바일 메모리를 상용화할 것으로 보인다. Sep 03, 2024 pm 02:15 PM

3일 홈페이지 보도에 따르면 국내 언론 에트뉴스는 어제(현지시간) 삼성전자와 SK하이닉스의 'HBM형' 적층구조 모바일 메모리 제품이 2026년 이후 상용화될 것이라고 보도했다. 소식통에 따르면 두 한국 메모리 거대 기업은 적층형 모바일 메모리를 미래 수익의 중요한 원천으로 여기고 'HBM형 메모리'를 스마트폰, 태블릿, 노트북으로 확장해 엔드사이드 AI에 전력을 공급할 계획이라고 전했다. 이 사이트의 이전 보도에 따르면 삼성전자 제품은 LPWide I/O 메모리라고 하며 SK하이닉스는 이 기술을 VFO라고 부른다. 두 회사는 팬아웃 패키징과 수직 채널을 결합하는 것과 거의 동일한 기술 경로를 사용했습니다. 삼성전자 LPWide I/O 메모리의 비트폭은 512이다.

삼성전자가 HBM4 메모리에 널리 사용될 것으로 예상되는 16단 하이브리드 본딩 적층 공정 기술 검증을 완료했다고 밝혔다. 삼성전자가 HBM4 메모리에 널리 사용될 것으로 예상되는 16단 하이브리드 본딩 적층 공정 기술 검증을 완료했다고 밝혔다. Apr 07, 2024 pm 09:19 PM

보고서에 따르면 삼성전자 김대우 상무는 2024년 한국마이크로전자패키징학회 연차총회에서 삼성전자가 16단 하이브리드 본딩 HBM 메모리 기술 검증을 완료할 것이라고 밝혔다. 해당 기술은 기술검증을 통과한 것으로 알려졌다. 보고서는 이번 기술 검증이 향후 몇 년간 메모리 시장 발전의 초석을 마련하게 될 것이라고 밝혔다. 김대우 사장은 삼성전자가 하이브리드 본딩 기술을 바탕으로 16단 적층 HBM3 메모리를 성공적으로 제조했다고 밝혔다. ▲이미지 출처 디일렉, 아래와 동일 하이브리드 본딩은 DRAM 메모리층 사이에 범프를 추가할 필요 없이 상하층 구리를 직접 연결하는 방식이다.

Lexar, Ares Wings of War DDR5 7600 16GB x2 메모리 키트 출시: 하이닉스 A-다이 입자, 1,299위안 Lexar, Ares Wings of War DDR5 7600 16GB x2 메모리 키트 출시: 하이닉스 A-다이 입자, 1,299위안 May 07, 2024 am 08:13 AM

5월 6일 이 웹사이트의 소식에 따르면 Lexar는 Ares Wings of War 시리즈 DDR57600CL36 오버클럭 메모리를 출시했습니다. 16GBx2 세트는 5월 7일 0시에 예약 판매가 가능하며 가격은 50위안입니다. 1,299위안. Lexar Wings of War 메모리는 Hynix A-die 메모리 칩을 사용하고 Intel XMP3.0을 지원하며 다음 두 가지 오버클러킹 사전 설정을 제공합니다. 7600MT/s: CL36-46-46-961.4V8000MT/s: CL38-48-49 -1001.45V 방열 측면에서는 이 메모리 세트에는 1.8mm 두께의 올 알루미늄 방열 조끼가 장착되어 있으며 PMIC 독점 열 전도성 실리콘 그리스 패드가 장착되어 있습니다. 메모리는 8개의 고휘도 LED 비드를 사용하고 13개의 RGB 조명 모드를 지원합니다.

MIT의 최신 걸작: GPT-3.5를 사용하여 시계열 이상 탐지 문제 해결 MIT의 최신 걸작: GPT-3.5를 사용하여 시계열 이상 탐지 문제 해결 Jun 08, 2024 pm 06:09 PM

오늘은 지난 주 MIT에서 발표한 기사를 소개하고자 합니다. GPT-3.5-turbo를 사용하여 시계열 이상 탐지 문제를 해결하고, 시계열 이상 탐지에서 LLM의 효율성을 초기에 검증한 내용입니다. 전체 과정에 미세한 조정은 없으며, 이상 탐지를 위해 GPT-3.5-turbo를 직접 사용하는 것이 이 글의 핵심이다. LLM이 이상 탐지 작업을 해결하도록 하는 프롬프트 또는 파이프라인입니다. 이 작품을 자세히 소개하겠습니다. 이미지 논문 제목: Large Languagemodelscanbezero-shotanomalydete

Kingbang은 CAMM2, LPCAM2 및 일반 모델 중에서 선택할 수 있는 새로운 DDR5 8600 메모리를 출시했습니다. Kingbang은 CAMM2, LPCAM2 및 일반 모델 중에서 선택할 수 있는 새로운 DDR5 8600 메모리를 출시했습니다. Jun 08, 2024 pm 01:35 PM

6월 7일 이 사이트의 소식에 따르면 GEIL은 2024년 타이페이 국제 컴퓨터 쇼에서 최신 DDR5 솔루션을 출시했으며 선택할 수 있는 SO-DIMM, CUDIMM, CSODIMM, CAMM2 및 LPCAM2 버전을 제공했습니다. ▲사진출처: Wccftech 사진에서 볼 수 있듯이 진방이 전시한 CAMM2/LPCAMM2 메모리는 매우 컴팩트한 디자인을 채택해 최대 128GB의 용량과 최대 8533MT/s의 속도를 제공할 수 있다. 보조 냉각 없이 9000MT/s까지 오버클럭된 AMDAM5 플랫폼에서 안정적입니다. 보고서에 따르면 Jinbang의 2024 Polaris RGBDDR5 시리즈 메모리는 최대 8400을 제공할 수 있습니다.

AI 물결의 영향은 분명합니다. TrendForce는 이번 분기에 DRAM 메모리 및 NAND 플래시 메모리 계약 가격 인상에 대한 예측을 수정했습니다. AI 물결의 영향은 분명합니다. TrendForce는 이번 분기에 DRAM 메모리 및 NAND 플래시 메모리 계약 가격 인상에 대한 예측을 수정했습니다. May 07, 2024 pm 09:58 PM

TrendForce 조사 보고서에 따르면 AI 물결은 DRAM 메모리와 NAND 플래시 메모리 시장에 상당한 영향을 미칩니다. 5월 7일 이 사이트의 뉴스에서 트렌드포스는 오늘 최신 연구 보고서에서 이번 분기에 두 가지 유형의 스토리지 제품에 대한 계약 가격 인상을 인상했다고 밝혔습니다. 구체적으로 트렌드포스는 당초 2024년 2분기 DRAM 메모리 계약 가격이 3~8% 인상될 것으로 추정했는데, 현재 NAND 플래시 메모리 기준으로는 13~18% 증가할 것으로 추정하고 있다. ~18%이고 새로운 추정치는 15% ~20%이며 eMMC/UFS만 10%의 더 낮은 증가율을 갖습니다. ▲이미지 출처 TrendForce TrendForce는 소속사가 당초 계속해서

Vivo의 새로운 X100 시리즈 메모리, 색상 노출: 모든 시리즈는 12+256GB부터 시작 Vivo의 새로운 X100 시리즈 메모리, 색상 노출: 모든 시리즈는 12+256GB부터 시작 May 06, 2024 pm 03:58 PM

5월 6일 뉴스에 따르면, vivo는 새로운 vivo X100 시리즈가 5월 13일 19시에 공식 출시된다고 오늘 공식 발표했습니다. 이번 컨퍼런스에서는 vivoX100s, vivoX100sPro, vivoX100Ultra 등 3가지 모델과 비보가 자체 개발한 이미징 브랜드 BlueImage 블루프린트 이미징 기술이 공개될 것으로 예상된다. 디지털 블로거 '디지털 채팅 스테이션'도 오늘 이 세 가지 모델의 공식 렌더링, 메모리 사양, 색상 매칭을 공개했습니다. 그 중 X100s는 직선형 화면 디자인을 채택한 반면, X100sPro와 X100Ultra는 곡선형 화면 디자인을 채택했습니다. 블로거는 vivoX100s가 블랙, 티타늄, 시안, 화이트 등 4가지 색상으로 출시된다고 밝혔습니다.

See all articles