百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术分析 > 正文

进程间通信之信号量semaphore--linux内核剖析

liebian365 2024-11-03 15:46 4 浏览 0 评论

什么是信号量


信号量的使用主要是用来保护共享资源,使得资源在一个时刻只有一个进程(线程)所拥有。

信号量的值为正的时候,说明它空闲。所测试的线程可以定而使用它。若为0,说明它被占用,测试的线程要进入睡眠队列中,等待被唤醒。

为了防止出现因多个程序同时访问一个共享资源而引发的一系列问题,我们需要一种方法,它可以通过生成并使用令牌来授权,在任一时刻只能有一个执行线程访问代码的临界区域

临界区域是指执行数据更新的代码需要独占式地执行。而信号量就可以提供这样的一种访问机制,让一个临界区同一时间只有一个线程在访问它,也就是说信号量是用来调协进程对共享资源的访问的。

信号量是一个特殊的变量,程序对其访问都是原子操作,且只允许对它进行等待(即P(信号变量))和发送(即V(信号变量))信息操作。

最简单的信号量是只能取0和1的变量,这也是信号量最常见的一种形式,叫做二进制信号量。而可以取多个正整数的信号量被称为通用信号量。这里主要讨论二进制信号量。

信号量的工作原理


由于信号量只能进行两种操作等待和发送信号,即P(sv)和V(sv),他们的行为是这样的:

  • P(sv):如果sv的值大于零,就给它减1;如果它的值为零,就挂起该进程的执行
  • V(sv):如果有其他进程因等待sv而被挂起,就让它恢复运行,如果没有进程因等待sv而挂起,就给它加1.

举个例子,就是两个进程共享信号量sv,一旦其中一个进程执行了P(sv)操作,它将得到信号量,并可以进入临界区,使sv减1。而第二个进程将被阻止进入临界区,因为当它试图执行P(sv)时,sv为0,它会被挂起以等待第一个进程离开临界区域并执行V(sv)释放信号量,这时第二个进程就可以恢复执行。

信号量的分类


在学习信号量之前,我们必须先知道——Linux提供两种信号量:

  • 内核信号量,由内核控制路径使用
  • 用户态进程使用的信号量,这种信号量又分为POSIX信号量和SYSTEM V信号量。

POSIX信号量又分为有名信号量无名信号量
有名信号量,其值保存在文件中, 所以它可以用于线程也可以用于进程间的同步。无名信号量,其值保存在内存中。

POSIX 信号量与SYSTEM V信号量的比较


  1. 对POSIX来说,信号量是个非负整数。常用于线程间同步。
    而SYSTEM V信号量则是一个或多个信号量的集合,它对应的是一个信号量结构体,这个结构体是为SYSTEM V IPC服务的,信号量只不过是它的一部分。常用于进程间同步。
  2. POSIX信号量的引用头文件是<semaphore.h>,而SYSTEM V信号量的引用头文件是<sys/sem.h>
  3. 从使用的角度,System V信号量是复杂的,而Posix信号量是简单。比如,POSIX信号量的创建和初始化或PV操作就很非常方便。

内核信号量


Linux内核的信号量在概念和原理上与用户态的System V的IPC机制信号量是一样的,但是它绝不可能在内核之外使用,它是一种睡眠锁。

如果有一个任务想要获得已经被占用的信号量时,信号量会将其放入一个等待队列(它不是站在外面痴痴地等待而是将自己的名字写在任务队列中)然后让其睡眠。

当持有信号量的进程将信号释放后,处于等待队列中的一个任务将被唤醒(因为队列中可能不止一个任务),并让其获得信号量。

这一点与自旋锁不同,处理器可以去执行其它代码。

它与自旋锁的差异:由于争用信号量的进程在等待锁重新变为可用时会睡眠,所以信号量适用于锁会被长时间持有的情况

相反,锁被短时间持有时,使用信号量就不太适宜了,因为睡眠、维护等待队列以及唤醒所花费的开销可能比锁占用的全部时间表还要长;

由于执行线程在锁被争用时会睡眠,所以只能在进程上下文中才能获得信号量锁,因为在中断上下文中是不能进行调试的;持有信号量的进行也可以去睡眠,当然也可以不睡眠,因为当其他进程争用信号量时不会因此而死锁;不能同时占用信号量和自旋锁,因为自旋锁不可以睡眠而信号量锁可以睡眠。相对而来说信号量比较简单,它不会禁止内核抢占,持有信号量的代码可以被抢占。

信号量还有一个特征,就是它允许多个持有者,而自旋锁在任何时候只能允许一个持有者。

当然我们经常遇到也是只有一个持有者,这种信号量叫二值信号量或者叫互斥信号量。允许有多个持有者的信号量叫计数信号量,在初始化时要说明最多允许有多少个持有者(Count值)
信号量在创建时需要设置一个初始值,表示同时可以有几个任务可以访问该信号量保护的共享资源,初始值为1就变成互斥锁(Mutex),即同时只能有一个任务可以访问信号量保护的共享资源。
当任务访问完被信号量保护的共享资源后,必须释放信号量,释放信号量通过把信号量的值加1实现,如果信号量的值为非正数,表明有任务等待当前信号量,因此它也唤醒所有等待该信号量的任务。

需要C/C++ Linux服务器架构师学习资料私信“资料”(资料包括C/C++,Linux,golang技术,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK,ffmpeg等),免费分享

内核信号量的构成


内核信号量类似于自旋锁,因为当锁关闭着时,它不允许内核控制路径继续进行。然而,当内核控制路径试图获取内核信号量锁保护的忙资源时,相应的进程就被挂起。只有在资源被释放时,进程才再次变为可运行。
只有可以睡眠的函数才能获取内核信号量;中断处理程序和可延迟函数都不能使用内核信号量。
内核信号量是
struct semaphore类型的对象,在内核源码中位于include\linux\semaphore.h文件

struct semaphore
{
   atomic_t count;
   int sleepers;
   wait_queue_head_t wait;
}

内核信号量中的等待队列


上面已经提到了内核信号量使用了等待队列wait_queue来实现阻塞操作。

当某任务由于没有某种条件没有得到满足时,它就被挂到等待队列中睡眠。当条件得到满足时,该任务就被移出等待队列,此时并不意味着该任务就被马上执行,因为它又被移进工作队列中等待CPU资源,在适当的时机被调度。

内核信号量是在内部使用等待队列的,也就是说该等待队列对用户是隐藏的,无须用户干涉。由用户真正使用的等待队列我们将在另外的篇章进行详解。

内核信号量的相关函数


初始化


#define __SEMAPHORE_INITIALIZER(name, n)                                \
{                                                                       \
        .lock           = __SPIN_LOCK_UNLOCKED((name).lock),            \
        .count          = n,                                            \
        .wait_list      = LIST_HEAD_INIT((name).wait_list),             \
}

该宏声明一个信号量name是直接将结构体中count值设置成n,此时信号量可用于实现进程间的互斥量。

#define DECLARE_MUTEX(name)     \
        struct semaphore name = __SEMAPHORE_INITIALIZER(name, 1)

该宏声明一个互斥锁name,但把它的初始值设置为1

void sema_init (struct semaphore *sem, int val);

该函用于数初始化设置信号量的初值,它设置信号量sem的值为val。

#define init_MUTEX(sem)         sema_init(sem, 1)

该函数用于初始化一个互斥锁,即它把信号量sem的值设置为1。

#define init_MUTEX_LOCKED(sem)  sema_init(sem, 0)

该函数也用于初始化一个互斥锁,但它把信号量sem的值设置为0,即一开始就处在已锁状态。

注意:对于信号量的初始化函数Linux最新版本存在变化,如init_MUTEX和init_MUTEX_LOCKED等初始化函数目前新的内核中已经没有或者更换了了名字等

因此建议以后在编程中遇到需要使用信号量的时候尽量采用sema_init(struct semaphore *sem, int val)函数,因为这个函数就目前为止从未发生变化。

获取信号量–申请内核信号量所保护的资源


void down(struct semaphore * sem);

该函数用于获得信号量sem,它会导致睡眠,因此不能在中断上下文(包括IRQ上下文和softirq上下文)使用该函数。该函数将把sem的值减1,如果信号量sem的值非负,就直接返回,否则调用者将被挂起,直到别的任务释放该信号量才能继续运行。

int down_interruptible(struct semaphore * sem);

该函数功能与down类似,不同之处为,down不会被信号(signal)打断,但down_interruptible能被信号(比如Ctrl+C)打断,因此该函数有返回值来区分是正常返回还是被信号中断,如果返回0,表示获得信号量正常返回,如果被信号打断,返回-EINTR

int down_trylock(struct semaphore * sem);

该函数试着获得信号量sem,如果能够立刻获得,它就获得该信号量并返回0,否则,表示不能获得信号量sem,返回值为非0值。因此,它不会导致调用者睡眠,可以在中断上下文使用

释放内核信号量所保护的资源


void up(struct semaphore * sem);

该函数释放信号量sem,即把sem的值加1,如果sem的值为非正数,表明有任务等待该信号量,因此唤醒这些等待者。

内核信号量的使用例程


在驱动程序中,当多个线程同时访问相同的资源时(驱动中的全局变量时一种典型的
共享资源),可能会引发“竞态“,因此我们必须对共享资源进行并发控制。Linux内核中
解决并发控制的最常用方法是自旋锁与信号量(绝大多数时候作为互斥锁使用)。

ssize_t globalvar_write(struct file *filp, const char *buf, size_t len, loff_t *off)
{
 //获得信号量
 if (down_interruptible(&sem))
 {
  return - ERESTARTSYS;
 }
 //将用户空间的数据复制到内核空间的global_var
 if (copy_from_user(&global_var, buf, sizeof(int)))
 {
  up(&sem);
  return - EFAULT;
 }
 //释放信号量
 up(&sem);
 return sizeof(int);
}

读-写信号量


跟自旋锁一样,信号量也有区分读-写信号量之分

如果一个读写信号量当前没有被写者拥有并且也没有写者等待读者释放信号量,那么任何读者都可以成功获得该读写信号量;

否则,读者必须被挂起直到写者释放该信号量。如果一个读写信号量当前没有被读者或写者拥有并且也没有写者等待该信号量,那么一个写者可以成功获得该读写信号量,否则写者将被挂起,直到没有任何访问者。因此,写者是排他性的,独占性的。

读写信号量有两种实现,一种是通用的,不依赖于硬件架构,因此,增加新的架构不需要重新实现它,但缺点是性能低,获得和释放读写信号量的开销大;另一种是架构相关的,因此性能高,获取和释放读写信号量的开销小,但增加新的架构需要重新实现。在内核配置时,可以通过选项去控制使用哪一种实现。
读写信号量的相关API有:

DECLARE_RWSEM(name)

该宏声明一个读写信号量name并对其进行初始化。

void init_rwsem(struct rw_semaphore *sem);

该函数对读写信号量sem进行初始化。

void down_read(struct rw_semaphore *sem);

读者调用该函数来得到读写信号量sem。该函数会导致调用者睡眠,因此只能在进程上下文使用。

int down_read_trylock(struct rw_semaphore *sem);

该函数类似于down_read,只是它不会导致调用者睡眠。它尽力得到读写信号量sem,如果能够立即得到,它就得到该读写信号量,并且返回1,否则表示不能立刻得到该信号量,返回0。因此,它也可以在中断上下文使用。

void down_write(struct rw_semaphore *sem);

写者使用该函数来得到读写信号量sem,它也会导致调用者睡眠,因此只能在进程上下文使用。

int down_write_trylock(struct rw_semaphore *sem);

该函数类似于down_write,只是它不会导致调用者睡眠。该函数尽力得到读写信号量,如果能够立刻获得,就获得该读写信号量并且返回1,否则表示无法立刻获得,返回0。它可以在中断上下文使用。

void up_read(struct rw_semaphore *sem);

读者使用该函数释放读写信号量sem。它与down_read或down_read_trylock配对使用。如果down_read_trylock返回0,不需要调用up_read来释放读写信号量,因为根本就没有获得信号量。

void up_write(struct rw_semaphore *sem);

写者调用该函数释放信号量sem。它与down_write或down_write_trylock配对使用。如果down_write_trylock返回0,不需要调用up_write,因为返回0表示没有获得该读写信号量。

void downgrade_write(struct rw_semaphore *sem);

该函数用于把写者降级为读者,这有时是必要的。因为写者是排他性的,因此在写者保持读写信号量期间,任何读者或写者都将无法访问该读写信号量保护的共享资源,对于那些当前条件下不需要写访问的写者,降级为读者将,使得等待访问的读者能够立刻访问,从而增加了并发性,提高了效率。
读写信号量适于在读多写少的情况下使用,在linux内核中对进程的内存映像描述结构的访问就使用了读写信号量进行保护。
究竟什么时候使用自旋锁什么时候使用信号量,下面给出建议的方案
当对低开销、短期、中断上下文加锁,优先考虑自旋锁;当对长期、持有锁需要休眠的任务,优先考虑信号量。

POSIX信号量详解


无名信号量


无名信号量的创建就像声明一般的变量一样简单,例如:sem_t sem_id。然后再初始化该无名信号量,之后就可以放心使用了。

无名信号量常用于多线程间的同步,同时也用于相关进程间的同步。也就是说,无名信号量必须是多个进程(线程)的共享变量,无名信号量要保护的变量也必须是多个进程(线程)的共享变量,这两个条件是缺一不可的。

常见的无名信号量相关函数


int sem_init(sem_t *sem, int pshared, unsigned int value);
  • pshared==0 用于同一多线程的同步;
  • 若pshared>0 用于多个相关进程间的同步(即由fork产生的)
int sem_getvalue(sem_t *sem, int *sval);

取回信号量sem的当前值,把该值保存到sval中。
若有1个或更多的线程或进程调用sem_wait阻塞在该信号量上,该函数返回两种值:

  • 返回0
  • 返回阻塞在该信号量上的进程或线程数目

linux采用返回的第一种策略。

sem_wait(或sem_trywait)相当于P操作,即申请资源。

int sem_wait(sem_t *sem); // 这是一个阻塞的函数

测试所指定信号量的值,它的操作是原子的。

  • 若sem>0,那么它减1并立即返回。
  • 若sem==0,则睡眠直到sem>0,此时立即减1,然后返回。
int sem_trywait(sem_t *sem); // 非阻塞的函数

其他的行为和sem_wait一样,除了:
若sem==0,不是睡眠,而是返回一个错误EAGAIN。

sem_post相当于V操作,释放资源。

int sem_post(sem_t *sem);

把指定的信号量sem的值加1;

呼醒正在等待该信号量的任意线程。
注意:在这些函数中,只有sem_post是信号安全的函数,它是可重入函数

无名信号量在多线程间的同步


无名信号量的常见用法是将要保护的变量放在sem_wait和sem_post中间所形成的
临界区内,这样该变量就会被保护起来,例如:

#include <pthread.h>
#include <semaphore.h>
#include <sys/types.h>
#include <stdio.h>
#include <unistd.h>

int number; // 被保护的全局变量
sem_t sem_id;

void* thread_one_fun(void *arg)
{
    sem_wait(&sem_id);
    printf("thread_one have the semaphore\n");
    number++;

    printf("thread_one : number = %d\n", number);

    sem_post(&sem_id);

    return NULL;
}

void* thread_two_fun(void *arg)
{
    sem_wait(&sem_id);
    printf("thread_two have the semaphore \n");

    number--;
    printf("thread_two : number = %d\n", number);

    sem_post(&sem_id);

    return NULL;
}
int main(int argc, char *argv[])
{
    number = 1;
    pthread_t id1, id2;

    sem_init(&sem_id, 0, 1);

    pthread_create(&id1, NULL, thread_one_fun, NULL);
    pthread_create(&id2, NULL, thread_two_fun, NULL);

    pthread_join(id1, NULL);
    pthread_join(id2, NULL);

    printf("main...\n");

    return 0;
}

上面的例程,到底哪个线程先申请到信号量资源,这是随机的。

进程1先执行,进城2后执行

进程2先执行,进城1后执行

如果想要某个特定的顺序的话,可以用2个信号量来实现。例如下面的例程是线程1先执行完,然后线程2才继续执行,直至结束。

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <semaphore.h>
#include <sys/types.h>
#include <stdio.h>
#include <unistd.h>

int number;                 // 被保护的全局变量
sem_t sem_id1, sem_id2;



/*
 *  线程1,
 *  对sem_id1加锁(P操作)以后
 *  将number增加1
 *  同时对sem_id2进行释放,V操作
 *
 *  */
void* thread_one_fun(void *arg)
{
    sem_wait(&sem_id1);
    printf("thread_one have the semaphore\n");

    number++;

    printf("number = %d\n",number);
    sem_post(&sem_id2);

    return NULL;
}

/*
 *  线程2,
 *  对sem_id2加锁(P操作)以后
 *  将number减少1
 *  同时对sem_id1进行释放,V操作
 *
 *  */
void* thread_two_fun(void *arg)
{
    sem_wait(&sem_id2);
    printf("thread_two have the semaphore \n");

    number--;

    printf("number = %d\n",number);
    sem_post(&sem_id1);

    return NULL;
}

int main(int argc,char *argv[])
{
    number = 1;
    pthread_t id1, id2;

    /*
     *  由于程序初始时, sem_id1可进入, sem_id2不可进入
     *  两个线程的动作如下:
     *  thread one P(id1)  number++ V(id2)
     *  thread two P(id2)  number-- V(id1)
     *  而id1可进入, id2不可进入
     *  因此thread one先执行
     *  如果将id1与id2的顺序交换, 则执行顺序相反
     * */
    sem_init(&sem_id1, 0, 1);   // 空闲的
    sem_init(&sem_id2, 0, 0);   // 忙的

    pthread_create(&id1, NULL, thread_one_fun, NULL);
    pthread_create(&id2, NULL, thread_two_fun, NULL);

    pthread_join(id1, NULL);
    pthread_join(id2, NULL);

    printf("main...\n");


    return EXIT_SUCCESS;
}

无名信号量在相关进程间的同步


说是相关进程,是因为本程序中共有2个进程,其中一个是另外一个的子进程(由fork产生)的。

本来对于fork来说,子进程只继承了父进程的代码副本,mutex理应在父子进程中是相互独立的两个变量,但由于在初始化mutex的时候,由pshared = 1指定了mutex处于共享内存区域,所以此时mutex变成了父子进程共享的一个变量。此时,mutex就可以用来同步相关进程了。

#include <stdio.h>
#include <stdlib.h>

#include <semaphore.h>
#include <errno.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/mman.h>

int main(int argc, char **argv)
{
    int     fd, i;
    int     nloop = 10, zero = 0;
    int     *ptr;
    sem_t   mutex;

    //  open a file and map it into memory
    fd = open("log.txt", O_RDWR | O_CREAT, S_IRWXU);

    write(fd,&zero,sizeof(int));

    ptr = mmap(NULL, sizeof(int), PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);

    close(fd);

    /* create, initialize semaphore */
    if(sem_init(&mutex, 1, 1) < 0) //
    {
        perror("semaphore initilization");
        exit(0);
    }

    if (fork() == 0)
    {   /* child process*/
        for (i = 0; i < nloop; i++)
        {
            sem_wait(&mutex);

            printf("child: %d\n", (*ptr)++);
            //sleep(1);
            sem_post(&mutex);
        }
        exit(0);
    }


    /* back to parent process */
    for (i = 0; i < nloop; i++)
    {
        sem_wait(&mutex);

        printf("parent: %d\n", (*ptr)++);
        //sleep(1);
        sem_post(&mutex);
    }
    exit(0);
}

有名信号量


有名信号量的特点是把信号量的值保存在文件中。

这决定了它的用途非常广:既可以用于线程,也可以用于相关进程间,甚至是不相关进程。

有名信号量能在进程间共享的原因


由于有名信号量的值是保存在文件中的,所以对于相关进程来说,子进程是继承了父进程的文件描述符,那么子进程所继承的文件描述符所指向的文件是和父进程一样的,当然文件里面保存的有名信号量值就共享了。

有名信号量相关函数说明


有名信号量在使用的时候,和无名信号量共享sem_wait和sem_post函数。
区别是有名信号量使用sem_open代替sem_init,另外在结束的时候要像关闭文件一样去关闭这个有名信号量。

  • 打开一个已存在的有名信号量,或创建并初始化一个有名信号量。一个单一的调用就完
    成了信号量的创建、初始化和权限的设置。
sem_t *sem_open(const char *name, int oflag, mode_t mode , int value);

参数描述name文件的路径名;Oflag有O_CREAT或O_CREATmode_t控制新的信号量的访问权限;Value指定信号量的初始化值。

注意:

这里的name不能写成/tmp/aaa.sem这样的格式,因为在linux下,sem都是创建在/dev/shm目录下。你可以将name写成“/mysem”或“mysem”,创建出来的文件都是“/dev/shm/sem.mysem”,千万不要写路径。也千万不要写“/tmp/mysem”之类的。

当oflag = O_CREAT时,若name指定的信号量不存在时,则会创建一个,而且后面的mode和value参数必须有效。若name指定的信号量已存在,则直接打开该信号量,

同时忽略mode和value参数。

当oflag = O_CREAT|O_EXCL时,若name指定的信号量已存在,该函数会直接返回error。

  • 一旦你使用了一信号量,销毁它们就变得很重要。
    在做这个之前,要确定所有对这个有名信号量的引用都已经通过sem_close()函数关闭了,然后只需在退出或是退出处理函数中调用sem_unlink()去删除系统中的信号量,
    注意如果有任何的处理器或是线程引用这个信号量,sem_unlink()函数不会起到任何的作用。

也就是说,必须是最后一个使用该信号量的进程来执行sem_unlick才有效。因为每个信号灯有一个引用计数器记录当前的打开次数,sem_unlink必须等待这个数为0时才能把name所指的信号灯从文件系统中删除。也就是要等待最后一个sem_close发生。

有名信号量在无相关进程间的同步


前面已经说过,有名信号量是位于共享内存区的,那么它要保护的资源也必须是位于共享内存区,只有这样才能被无相关的进程所共享。
在下面这个例子中,服务进程和客户进程都使用
shmgetshmat来获取得一块共享内存资源。然后利用有名信号量来对这块共享内存资源进行互斥保护。

服务器程序

//server.c
#include <stdio.h>
#include <stdlib.h>

#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <semaphore.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

#define SHMSZ 27

char SEM_NAME[]= "vik";


int main()
{
    char    ch;
    int     shmid;
    key_t   key;
    char    *shm,*s;
    sem_t   *mutex;

    //name the shared memory segment
    key = 1000;

    //create & initialize semaphore
    mutex = sem_open(SEM_NAME, O_CREAT, 0644, 1);
    if(mutex == SEM_FAILED)
    {
        perror("unable to create semaphore");
        sem_unlink(SEM_NAME);

        exit(-1);
    }

    //create the shared memory segment with this key
    shmid = shmget(key, SHMSZ, IPC_CREAT | 0666);
    if(shmid < 0)
    {
        perror("failure in shmget");

        exit(-1);
    }

    //attach this segment to virtual memory
    shm = shmat(shmid, NULL, 0);
    //start writing into memory
    s = shm;
    for(ch = 'A'; ch <= 'Z'; ch++)
    {
        sem_wait(mutex);

        *s++ = ch;

        sem_post(mutex);
    }

    //the below loop could be replaced by binary semaphore
    while(*shm != '*')
    {
        sleep(1);
    }

    sem_close(mutex);

    sem_unlink(SEM_NAME);

    shmctl(shmid, IPC_RMID, 0);

    return EXIT_SUCCESS;

}

客户端程序

// client.c
#include <stdio.h>
#include <stdlib.h>


#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <stdio.h>
#include <semaphore.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

#define SHMSZ 27

char SEM_NAME[]= "vik";

int main()
{
    int     shmid;
    key_t   key;
    char    *shm, *s;
    sem_t   *mutex;

    //  name the shared memory segment
    key = 1000;

    //  create & initialize existing semaphore
    mutex = sem_open(SEM_NAME, 0, 0644, 0);
    if(mutex == SEM_FAILED)
    {
        perror("reader:unable to execute semaphore");
        sem_close(mutex);

        exit(-1);
    }

    //  create the shared memory segment with this key
    shmid = shmget(key, SHMSZ, 0666);
    if(shmid < 0)
    {
        perror("reader:failure in shmget");

        exit(-1);
    }

    //  attach this segment to virtual memory
    shm = shmat(shmid, NULL, 0);

    //  start reading
    s = shm;
    for(s = shm; *s != '\0'; s++)
    {
        sem_wait(mutex);

        putchar(*s);

        sem_post(mutex);
    }

    //  once done signal exiting of reader:This can be replaced by another semaphore
    *shm = '*';
    sem_close(mutex);
    shmctl(shmid, IPC_RMID, 0);

    return EXIT_SUCCESS;
}

SYSTEM V信号量


这是信号量值的集合,而不是单个信号量。相关的信号量操作函数由<sys/ipc.h>引用。

ystem V 信号量在内核中维护,其中包括二值信号量 、计数信号量、计数信号量集。

  • 二值信号量 : 其值只有0、1 两种选择,0表示资源被锁,1表示资源可用;
  • 计数信号量:其值在0 和某个限定值之间,不限定资源数只在0 1 之间;
  • 计数信号量集 :多个信号量的集合组成信号量集

信号量结构体


内核为每个信号量集维护一个信号量结构体,可在

struct semid_ds
{
    struct ipc_perm sem_perm; /* 信号量集的操作许可权限 */
    struct sem *sem_base; /* 某个信号量sem结构数组的指针,当前信号量集中的每个信号量对应其中一个数组元素 */
    ushort sem_nsems; /* sem_base 数组的个数 */
    time_t sem_otime; /* 最后一次成功修改信号量数组的时间 */
    time_t sem_ctime; /* 成功创建时间 */
};

其中ipc_perm 结构是内核给每个进程间通信对象维护的一个信息结构,其成员包含所有者用户id,所有者组id、创建者及其组id,以及访问模式等;semid_ds结构体中的sem结构是内核用于维护某个给定信号量的一组值的内部结构,其结构定义:

struct sem {
ushort semval; /* 信号量的当前值 */
short sempid; /* 最后一次返回该信号量的进程ID 号 */
ushort semncnt; /* 等待semval大于当前值的进程个数 */
ushort semzcnt; /* 等待semval变成0的进程个数 */
};

常见的SYSTEM V信号量函数


关键字和描述符


SYSTEM V信号量是SYSTEM V IPC(即SYSTEM V进程间通信)的组成部分,其他的有SYSTEM V消息队列,SYSTEM V共享内存。而关键字和IPC描述符无疑是它们的共同点,也使用它们,就不得不先对它们进行熟悉。这里只对SYSTEM V信号量进行讨论。

IPC描述符相当于引用ID号,要想使用SYSTEM V信号量(或MSG、SHM),就必须用IPC描述符来调用信号量。而IPC描述符是内核动态提供的(通过semget来获取),用户无法让服务器和客户事先认可共同使用哪个描述符,所以有时候就需要到关键字KEY来定位描述符。

某个KEY只会固定对应一个描述符(这项转换工作由内核完成),这样假如服务器和

客户事先认可共同使用某个KEY,那么大家就都能定位到同一个描述符,也就能定位到同一个信号量,这样就达到了SYSTEM V信号量在进程间共享的目的。

创建和打开信号量


创建一个信号量或访问一个已经存在的信号量集。

int semget(key_t key, int nsems, int oflag)

该函数执行成功返回信号量标示符,失败返回-1

参数描述key通过调用ftok函数得到的键值nsems代表创建信号量的个数,如果只是访问而不创建则可以指定该参数为0,我们一旦创建了该信号量,就不能更改其信号量个数,只要你不删除该信号量,你就是重新调用该函数创建该键值的信号量,该函数只是返回以前创建的值,不会重新创建;semflg指定该信号量的读写权限,当创建信号量时不许加IPC_CREAT ,若指定IPC_CREAT

semget函数执行成功后,就产生了一个由内核维持的类型为semid_ds结构体的信号量集,返回semid就是指向该信号量集的引索。

  • nsems>0 : 创建一个信的信号量集,指定集合中信号量的数量,一旦创建就不能更改。
  • nsems==0 : 访问一个已存在的集合
  • 返回的是一个称为信号量标识符的整数,semop和semctl函数将使用它。
  • 创建成功后信号量结构被设置:
.sem_perm 的uid和gid成员被设置成的调用进程的有效用户ID和有效组ID
.oflag 参数中的读写权限位存入sem_perm.mode
.sem_otime 被置为0,sem_ctime被设置为当前时间
.sem_nsems 被置为nsems参数的值1234

该集合中的每个信号量不初始化,这些结构是在semctl,用参数SET_VAL,SETALL初始化的。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/sem.h>
#include <sys/ipc.h>

#define SEM_R    0400   //用户(属主)读
#define SEM_A    0200   //用户(属主)写

#define SVSEM_MODE (SEM_R | SEM_A | SEM_R>>3 | SEM_R>>6)

int main(int argc,char *argv[])
{
    int   c, oflag, semid, nsems;

    oflag = SVSEM_MODE | IPC_CREAT;   //设置创建模式

    //根据命令行参数e判断是否制定了IPC_EXCL模式
    while((c = getopt(argc,argv,"e")) != -1)
    {
        switch(c)
        {
            case 'e':
                oflag |= IPC_EXCL;
                break;
        }
    }

    //判断命令行参数是否合法
    if (optind != argc -2)
    {
        printf("usage: semcreate [-e] <pathname> <nsems>");
        exit(-1);
    }

    //获取信号量集合中的信号量个数
    nsems = atoi(argv[optind+1]);

    //创建信号量,通过ftok函数创建一个key,返回信号量 标识符
    semid = semget(ftok(argv[optind],0),nsems,oflag);


    return EXIT_SUCCESS;
}

关键字的获取


有多种方法使客户机和服务器在同一IPC结构上会合:
* 服务器可以指定关键字IPC_PRIVATE创建一个新IPC结构,将返回的标识符存放在某处(例如一个文件)以便客户机取用。关键字 IPC_PRIVATE保证服务器创建一个新IPC结构。这种技术的缺点是:服务器要将整型标识符写到文件中,然后客户机在此后又要读文件取得此标识符。

IPC_PRIVATE关键字也可用于父、子关系进程。父进程指定 IPC_PRIVATE创建一个新IPC结构,所返回的标识符在fork后可由子进程使用。子进程可将此标识符作为exec函数的一个参数传给一个新程序。

  • 在一个公用头文件中定义一个客户机和服务器都认可的关键字。然后服务器指定此关键字创建一个新的IPC结构。这种方法的问题是该关键字可能已与一个 IPC结构相结合,在此情况下,get函数(msgget、semget或shmget)出错返回。服务器必须处理这一错误,删除已存在的IPC结构,然后试着再创建它。当然,这个关键字不能被别的程序所占用。
  • 客户机和服务器认同一个路径名和课题I D(课题I D是0 ~ 2 5 5之间的字符值) ,然后调用函数ftok将这两个值变换为一个关键字。这样就避免了使用一个已被占用的关键字的问题。
    使用ftok并非高枕无忧。有这样一种例外:服务器使用ftok获取得一个关键字后,该文件就被删除了,然后重建。此时客户端以此重建后的文件来ftok所获取的关键字就和服务器的关键字不一样了。所以一般商用的软件都不怎么用ftok。
    一般来说,客户机和服务器至少共享一个头文件,所以一个比较简单的方法是避免使用ftok,而只是在该头文件中存放一个大家都知道的关键字。

设置信号量的值(PV操作)


int semop(int semid, struct sembuf *opsptr, size_t nops);

参数描述semid是semget返回的semid信号量标示符opsptr指向信号量操作结构数组nopsopsptr所指向的数组中的sembuf结构体的个数

该函数执行成功返回0,失败返回-1;

第二个参数sops为一个结构体数组指针,结构体定义在sys/sem.h中,结构体如下

struct sembuf {
   unsigned short sem_num; /* semaphore index in array */
   short sem_op; /* semaphore operation */
   short sem_flg; /* operation flags */
};

sem_num 操作信号的下标,其值可以为0 到nops
sem_flg为该信号操作的标志:其值可以为0、IPC_NOWAIT 、 SEM_UNDO

sem_flg标识描述0在对信号量的操作不能执行的情况下,该操作阻塞到可以执行为止;IPC_NOWAIT在对信号量的操作不能执行的情况下,该操作立即返回;SEM_UNDO当操作的进程推出后,该进程对sem进行的操作将被取消;

sem_op取值描述>0>0则信号量加上它的值,等价于进程释放信号量控制的资源=0=0若没有设置IPC_NOWAIT, 那么调用进程将进入睡眠状态,直到信号量的值为0,否则进程直接返回<script type="math/tex" id="MathJax-Element-19">< 0</script>则信号量加上它的值,等价于进程申请信号量控制的资源,若进程设置IPC_NOWAIT则进程再没有可用资源情况下,进程阻塞,否则直接返回。

例如,当前semval为2,而sem_op = -3,那么怎么办?

注意:semval是指semid_ds中的信号量集中的某个信号量的值

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/sem.h>
#include <sys/ipc.h>

int main(int argc,char *argv[])
{
    int     c,i,flag,semid,nops;
    struct  sembuf *ptr;
    flag = 0;
        //根据命令行参数设置操作模式
    while( ( c = getopt(argc,argv,"nu")) != -1)
    {
        switch(c)
        {
            case 'n':
                flag |= IPC_NOWAIT;   //非阻塞
                break;
            case 'u':
                flag |= SEM_UNDO;   //不可恢复
                break;
        }
    }
    if(argc - optind < 2)
    {
        printf("usage: semops [-n] [-u] <pathname> operation...");
        exit(0);
    }
    //打开一个已经存在的信号量集合
    if((semid = semget(ftok(argv[optind],0),0,0)) == -1)
    {
        perror("semget() error");
        exit(-1);
    }
    optind++;  //指向当前第一个信号量的位置
    nops = argc - optind;   //信号量个数
    ptr = calloc(nops,sizeof(struct sembuf));
    for(i=0;i<nops;++i)
    {
        ptr[i].sem_num = i;  //信号量变换
        ptr[i].sem_op = atoi(argv[optind+i]);   //设置信号量的值
        ptr[i].sem_flg = flag;   //设置操作模式
    }
    //对信号量执行操作
    if(semop(semid,ptr,nops) == -1)
    {
        perror("semop() error");
        exit(-1);
    }

    return EXIT_SUCCESS;
}

对信号集实行控制操作(semval的赋值等)


int semctl(int semid, int semum, int cmd, ../* union semun arg */);

参数描述semid是信号量集合;semnum是信号在集合中的序号;semum是一个必须由用户自定义的结构体,在这里我们务必弄清楚该结构体的组成:

union semun
{
int val; // cmd == SETVAL
struct semid_ds *buf // cmd == IPC_SET或者 cmd == IPC_STAT
ushort *array; // cmd == SETALL,或 cmd = GETALL
};

值描述IPC_STAT读取一个信号量集的数据结构semid_ds,并将其存储在semun中的buf参数中。IPC_SET设置信号量集的数据结构semid_ds中的元素ipc_perm,其值取自semun中的buf参数。IPC_RMID将信号量集从系统中删除GETALL用于读取信号量集中的所有信号量的值,存于semnu的array中SETALL设置所指定的信号量集的每个成员semval的值GETPID返回最后一个执行semop操作的进程的PID。LSETVAL把的val数据成员设置为当前资源数GETVAL把semval中的当前值作为函数的返回,即现有的资源数,返回值为非负数。

val只有cmd ==SETVAL时才有用,此时指定的semval = arg.val。

注意:当cmd == GETVAL时,semctl函数返回的值就是我们想要的semval。千万不要以为指定的semval被返回到arg.val中。

array指向一个数组,

当cmd==SETALL时,就根据arg.array来将信号量集的所有值都赋值;

当cmd ==GETALL时,就将信号量集的所有值返回到arg.array指定的数组中。

buf 指针只在cmd==IPC_STAT 或IPC_SET 时有用,作用是semid 所指向的信号量集

(semid_ds机构体)。一般情况下不常用,这里不做谈论。

另外,cmd == IPC_RMID还是比较有用的。

示例程序


调用semctl函数设置信号量的值程序


#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/sem.h>
#include <sys/ipc.h>

//定义信号量操作共用体结构
union semun
{
    int                val;
    struct semid_ds    *buf;
    unsigned short     *array;
};

int main(int argc,char *argv[])
{
    int semid,nsems,i;
    struct semid_ds seminfo;
    unsigned short *ptr;
    union semun arg;

    if(argc < 2)
    {
            printf("usage: semsetvalues <pathname>[values ...]");
            exit(0);
    }

    //打开已经存在的信号量集合
    semid = semget(ftok(argv[1],0),0,0);
    arg.buf = &seminfo;

    //获取信号量集的相关信息
    semctl(semid,0,IPC_STAT,arg);
    nsems = arg.buf->sem_nsems;  //信号量的个数
    if(argc != nsems + 2 )
    {
        printf("%s semaphores in set,%d values specified",nsems,argc-2);
        exit(0);
    }

    //分配信号量
    ptr = calloc(nsems,sizeof(unsigned short));
    arg.array = ptr;

    //初始化信号量的值
    for(i=0;i<nsems;i++)
    {
        ptr[i] = atoi(argv[i+2]);
    }


    //通过arg设置信号量集合
    semctl(semid,0,SETALL,arg);

    return EXIT_SUCCESS;
}

调用semctl获取信号量的值


#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/sem.h>
#include <sys/ipc.h>

union semun
{
    int             val;
    struct semid_ds *buf;
    unsigned short     *array;
};

int main(int argc,char *argv[])
{
    int     semid,nsems,i;
    struct semid_ds seminfo;
    unsigned short *ptr;
    union semun arg;

    if(argc != 2)
    {
        printf("usage: semgetvalues<pathname>");
        exit(0);
    }

    //打开已经存在的信号量
    semid = semget(ftok(argv[1], 0), 0, 0);
    arg.buf = &seminfo;

    //获取信号量集的属性,返回semid_ds结构
    semctl(semid, 0, IPC_STAT, arg);
    nsems = arg.buf->sem_nsems;     //信号量的数目

    ptr = calloc(nsems,sizeof(unsigned short));
    arg.array = ptr;

    //获取信号量的值
    semctl(semid, 0, GETALL, arg);
    for(i = 0; i < nsems; i++)
    {
        printf("semval[%d] = %d\n", i, ptr[i]);
    }

    return EXIT_SUCCESS;
}

通过semctl实现PV操作的函数库

#include <semaphore.h>
#include <sys/sem.h>
#include <stdio.h>
#include <stdlib.h>

union semun
{
    int                 val;
    struct semid_ds     *buf;
    unsigned short      *array;
};

// 将信号量sem_id设置为init_value
int init_sem(int sem_id, int init_value)
{
    union semun     sem_union;
    sem_union.val = init_value;

    if (semctl(sem_id, 0, SETVAL, sem_union) == -1)
    {
        perror("Sem init");

        exit(1);
    }

    return 0;
}

// 删除sem_id信号量
int del_sem(int sem_id)
{
    union semun sem_union;
    if (semctl(sem_id, 0, IPC_RMID, sem_union) == -1)
    {
        perror("Sem delete");

        exit(1);
    }
    return 0;
}


// 对sem_id执行p操作
int sem_p(int sem_id)
{
    struct sembuf   sem_buf;
    sem_buf.sem_num = 0;//信号量编号
    sem_buf.sem_op  = -1;//P操作
    sem_buf.sem_flg = SEM_UNDO;//系统退出前未释放信号量,系统自动释放

    if (semop(sem_id, &sem_buf, 1) == -1)
    {
        perror("Sem P operation");

        exit(1);
    }

    return 0;
}


// 对sem_id执行V操作
int sem_v(int sem_id)
{
    struct sembuf   sem_buf;
    sem_buf.sem_num = 0;
    sem_buf.sem_op  = 1;//V操作
    sem_buf.sem_flg = SEM_UNDO;

    if (semop(sem_id, &sem_buf, 1) == -1)
    {
        perror("Sem V operation");

        exit(1);
    }

    return 0;
}
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <stdio.h>
static int nsems;
static int semflg;
static int semid;
int errno=0;
union semun {
int val;
struct semid_ds *buf;
unsigned short *array;
}arg;
int main()
{
struct sembuf sops[2]; //要用到两个信号量,所以要定义两个操作数组
int rslt;
unsigned short argarray[80];
arg.array = argarray;
semid = semget(IPC_PRIVATE, 2, 0666);
if(semid < 0 )
{
printf("semget failed. errno: %d\n", errno);
exit(0);
}
//获取0th信号量的原始值
rslt = semctl(semid, 0, GETVAL);
printf("val = %d\n",rslt);
//初始化0th信号量,然后再读取,检查初始化有没有成功
arg.val = 1; // 同一时间只允许一个占有者
semctl(semid, 0, SETVAL, arg);
rslt = semctl(semid, 0, GETVAL);
printf("val = %d\n",rslt);
sops[0].sem_num = 0;
sops[0].sem_op = -1;
sops[0].sem_flg = 0;
sops[1].sem_num = 1;
sops[1].sem_op = 1;
sops[1].sem_flg = 0;
rslt=semop(semid, sops, 1); //申请0th信号量,尝试锁定
if (rslt < 0 )
{
printf("semop failed. errno: %d\n", errno);
exit(0);
}
//可以在这里对资源进行锁定
sops[0].sem_op = 1;
semop(semid, sops, 1); //释放0th信号量
rslt = semctl(semid, 0, GETVAL);
printf("val = %d\n",rslt);
rslt=semctl(semid, 0, GETALL, arg);
if (rslt < 0)
{
printf("semctl failed. errno: %d\n", errno);
exit(0);
}
printf("val1:%d val2: %d\n",(unsigned int)argarray[0],(unsigned int)argarray[1]);
if(semctl(semid, 1, IPC_RMID) == -1)
{
    perror(“semctl failure while clearing reason”);
}
return(0);
}

信号量的牛刀小试——生产者与消费者问题


1.问题描述:
有一个长度为N的缓冲池为生产者和消费者所共有,只要缓冲池未满,生产者便可将
消息送入缓冲池;只要缓冲池未空,消费者便可从缓冲池中取走一个消息。生产者往缓冲池
放信息的时候,消费者不可操作缓冲池,反之亦然。

2.使用多线程和信号量解决该经典问题的互斥

#include <stdio.h>
#include <stdlib.h>


#include <pthread.h>
#include <stdio.h>
#include <semaphore.h>

#define BUFF_SIZE 10

char buffer[BUFF_SIZE];
char count;                         //  缓冲池里的信息数目
sem_t sem_mutex;                    //  生产者和消费者的互斥锁
sem_t p_sem_mutex;                  //  空的时候,对消费者不可进
sem_t c_sem_mutex;                  //  满的时候,对生产者不可进

void * Producer()
{
    while(1)
    {
        sem_wait(&p_sem_mutex);     //  当缓冲池未满时
        sem_wait(&sem_mutex);       //  等待缓冲池空闲

        count++;

        sem_post(&sem_mutex);

        if(count < BUFF_SIZE)       //  缓冲池未满
        {
            sem_post(&p_sem_mutex);
        }

        if(count > 0)               //  缓冲池不为空
        {
            sem_post(&c_sem_mutex);
        }
    }
}

void * Consumer()
{
    while(1)
    {
        sem_wait(&c_sem_mutex); //  缓冲池未空时
        sem_wait(&sem_mutex);   //  等待缓冲池空闲

        count--;
        sem_post(&sem_mutex);

        if(count > 0)
        {
            sem_post(&c_sem_mutex);
        }
    }

    return NULL;
}



int main()
{
    pthread_t ptid,ctid;

    //  initialize the semaphores
    //sem_init(&empty_sem_mutex, 0, 1);
    //sem_init(&full_sem_mutex, 0, `0);

    //creating producer and consumer threads
    if(pthread_create(&ptid, NULL,Producer, NULL))
    {
        printf("\n ERROR creating thread 1");
        exit(1);
    }

    if(pthread_create(&ctid, NULL,Consumer, NULL))
    {
        printf("\n ERROR creating thread 2");
        exit(1);
    }

    if(pthread_join(ptid, NULL)) /* wait for the producer to finish */
    {
        printf("\n ERROR joining thread");
        exit(1);
    }

    if(pthread_join(ctid, NULL)) /* wait for consumer to finish */
    {
        printf("\n ERROR joining thread");
        exit(1);
    }

    //sem_destroy(&empty_sem_mutex);
    //sem_destroy(&full_sem_mutex);
    //exit the main thread
    pthread_exit(NULL);

    return EXIT_SUCCESS;
}

相关推荐

快递查询教程,批量查询物流,一键管理快递

作为商家,每天需要查询许许多多的快递单号,面对不同的快递公司,有没有简单一点的物流查询方法呢?小编的回答当然是有的,下面随小编一起来试试这个新技巧。需要哪些工具?安装一个快递批量查询高手快递单号怎么快...

一键自动查询所有快递的物流信息 支持圆通、韵达等多家快递

对于各位商家来说拥有一个好的快递软件,能够有效的提高自己的工作效率,在管理快递单号的时候都需要对单号进行表格整理,那怎么样能够快速的查询所有单号信息,并自动生成表格呢?1、其实方法很简单,我们不需要一...

快递查询单号查询,怎么查物流到哪了

输入单号怎么查快递到哪里去了呢?今天小编给大家分享一个新的技巧,它支持多家快递,一次能查询多个单号物流,还可对查询到的物流进行分析、筛选以及导出,下面一起来试试。需要哪些工具?安装一个快递批量查询高手...

3分钟查询物流,教你一键批量查询全部物流信息

很多朋友在问,如何在短时间内把单号的物流信息查询出来,查询完成后筛选已签收件、筛选未签收件,今天小编就分享一款物流查询神器,感兴趣的朋友接着往下看。第一步,运行【快递批量查询高手】在主界面中点击【添...

快递单号查询,一次性查询全部物流信息

现在各种快递的查询方式,各有各的好,各有各的劣,总的来说,还是有比较方便的。今天小编就给大家分享一个新的技巧,支持多家快递,一次能查询多个单号的物流,还能对查询到的物流进行分析、筛选以及导出,下面一起...

快递查询工具,批量查询多个快递快递单号的物流状态、签收时间

最近有朋友在问,怎么快速查询单号的物流信息呢?除了官网,还有没有更简单的方法呢?小编的回答当然是有的,下面一起来看看。需要哪些工具?安装一个快递批量查询高手多个京东的快递单号怎么快速查询?进入快递批量...

快递查询软件,自动识别查询快递单号查询方法

当你拥有多个快递单号的时候,该如何快速查询物流信息?比如单号没有快递公司时,又该如何自动识别再去查询呢?不知道如何操作的宝贝们,下面随小编一起来试试。需要哪些工具?安装一个快递批量查询高手快递单号若干...

教你怎样查询快递查询单号并保存物流信息

商家发货,快递揽收后,一般会直接手动复制到官网上一个个查询物流,那么久而久之,就会觉得查询变得特别繁琐,今天小编给大家分享一个新的技巧,下面一起来试试。教程之前,我们来预览一下用快递批量查询高手...

简单几步骤查询所有快递物流信息

在高峰期订单量大的时候,可能需要一双手当十双手去查询快递物流,但是由于逐一去查询,效率极低,追踪困难。那么今天小编给大家分享一个新的技巧,一次能查询多个快递单号的物流,下面一起来学习一下,希望能给大家...

物流单号查询,如何查询快递信息,按最后更新时间搜索需要的单号

最近有很多朋友在问,如何通过快递单号查询物流信息,并按最后更新时间搜索出需要的单号呢?下面随小编一起来试试吧。需要哪些工具?安装一个快递批量查询高手快递单号若干怎么快速查询?运行【快递批量查询高手】...

连续保存新单号功能解析,导入单号查询并自动识别批量查快递信息

快递查询已经成为我们日常生活中不可或缺的一部分。然而,面对海量的快递单号,如何高效、准确地查询每一个快递的物流信息,成为了许多人头疼的问题。幸运的是,随着科技的进步,一款名为“快递批量查询高手”的软件...

快递查询教程,快递单号查询,筛选更新量为1的单号

最近有很多朋友在问,怎么快速查询快递单号的物流,并筛选出更新量为1的单号呢?今天小编给大家分享一个新方法,一起来试试吧。需要哪些工具?安装一个快递批量查询高手多个快递单号怎么快速查询?运行【快递批量查...

掌握批量查询快递动态的技巧,一键查找无信息记录的两种方法解析

在快节奏的商业环境中,高效的物流查询是确保业务顺畅运行的关键。作为快递查询达人,我深知时间的宝贵,因此,今天我将向大家介绍一款强大的工具——快递批量查询高手软件。这款软件能够帮助你批量查询快递动态,一...

从复杂到简单的单号查询,一键清除单号中的符号并批量查快递信息

在繁忙的商务与日常生活中,快递查询已成为不可或缺的一环。然而,面对海量的单号,逐一查询不仅耗时费力,还容易出错。现在,有了快递批量查询高手软件,一切变得简单明了。只需一键,即可搞定单号查询,一键处理单...

物流单号查询,在哪里查询快递

如果在快递单号多的情况,你还在一个个复制粘贴到官网上手动查询,是一件非常麻烦的事情。于是乎今天小编给大家分享一个新的技巧,下面一起来试试。需要哪些工具?安装一个快递批量查询高手快递单号怎么快速查询?...

取消回复欢迎 发表评论: