实现一个jstack

在聊Jstack得工作原理前呢,不如让我们先写一个简单的jstack玩玩。不用怕,很简单的,就几行代码的事,看:

public class MyJstack {

    public static void main(String[] args)throws Exception {
VirtualMachine virtualMachine = VirtualMachine.attach("6361");
HotSpotVirtualMachine hotSpotVirtualMachine = (HotSpotVirtualMachine)virtualMachine;
InputStream inputStream = hotSpotVirtualMachine.remoteDataDump(new String[]{}); byte[] buff = new byte[256];
int len;
do {
len = inputStream.read(buff);
if (len > 0) {
String respone = new String(buff, 0, len, "UTF-8");
System.out.print(respone);
}
} while(len > 0); inputStream.close();
virtualMachine.detach();
}
}

很简单吧,贴到你的开发环境里,运行就好了,别忘了把6361这个进程号换成你自己的Java进程号哦。

实现原理

jstack有两种实现方式,一种是基于attach api,其实现可以在tools.jar里找到;另一种是基于SA的实现,它被放在了sa-jdi.jar里。如果你通过idea搜索Jstack类,你会看到tools.jar和sa-jdi.jar各有一个Jstack类。

本文呢,就通过分析attch api的源码,来了解jstack的工作原理。

jstack本地源码实现

我们来看一下HotSpotVirtualMachine的remoteDataDump方法:

public InputStream remoteDataDump(Object... var1) throws IOException {
return this.executeCommand("threaddump", var1);
}

他是在执行一个叫threaddump的命令。沿着这个executeCommand方法继续往里追,会发现他是调用了如下方法:

InputStream execute(String var1, Object... var2) throws AgentLoadException, IOException {
assert var2.length <= 3; String var3;
synchronized(this) {
if (this.path == null) {
throw new IOException("Detached from target VM");
} var3 = this.path;
} int var4 = socket(); try {
connect(var4, var3);
} catch (IOException var9) {
close(var4);
throw var9;
} IOException var5 = null; try {
this.writeString(var4, "1");
this.writeString(var4, var1);

var1参数就是我们的threaddump指令,不难看出,这个方法是建立了一个socket连接,然后将threaddump指令发送给另一端,即我们要检查的jvm进程。

注意:限于篇幅我并没有贴整个方法代码。execute是HotSpotVirtualMachine的抽象方法,不同平台的jdk有不同的execute方法的实现,我这里的代码是mac下的execute实现,位于BsdVirtualMachine类中。

通过jtack本地源代码,我们大致可以粗略的认为:jstack就是通过与指定的jvm进程建立socket连接,然后发送指令,最后将jvm进程返回的内容打印出来。

JVM的源码实现

了解了jstack的本地源码,我们在看看jvm进程是如何处理的。

当我们使用Java命令启动jvm进程时,Java命令会加载虚拟机共享库,然后执行共享库里的JNI_CreateJavaVM方法完成虚拟机的创建,在JNI_CreateJavaVM方法里会调用如下代码,完成具体的一个创建过程:

result = Threads::create_vm((JavaVMInitArgs*) args, &can_try_again);

如果你有心,或许会留意到,在你启动一个jvm进程时,即便你什么线程也没创建,你用jstack查看还是有很多的线程,如:Signal Dispatcher,VM Thread,Attach Listener等等。当过阅读本文,你会了解到这三个线程的作用。

01 VM Thread线程

Threads::create_vm这个方法很长,接下来咱们跳出一些重要的段落,来分析分析。

// Create the VMThread
{ TraceTime timer("Start VMThread", TraceStartupTime);
VMThread::create();//创建Thread对象
Thread* vmthread = VMThread::vm_thread(); if (!os::create_thread(vmthread, os::vm_thread))//调用操作系统api创建线程
vm_exit_during_initialization("Cannot create VM thread. Out of system resources."); // Wait for the VM thread to become ready, and VMThread::run to initialize
// Monitors can have spurious returns, must always check another state flag
{
MutexLocker ml(Notify_lock);
os::start_thread(vmthread);//启动线程
while (vmthread->active_handles() == NULL) {
Notify_lock->wait();
}
}
}

通过注释,你也知道,这一段代码是从来创建VM Thread线程的。VMThread::create()完成了对现成的命名工作,代码如下:

void VMThread::create() {
assert(vm_thread() == NULL, "we can only allocate one VMThread");
_vm_thread = new VMThread(); // Create VM operation queue
_vm_queue = new VMOperationQueue();
guarantee(_vm_queue != NULL, "just checking"); _terminate_lock = new Monitor(Mutex::safepoint, "VMThread::_terminate_lock", true); if (UsePerfData) {
// jvmstat performance counters
Thread* THREAD = Thread::current();
_perf_accumulated_vm_operation_time =
PerfDataManager::create_counter(SUN_THREADS, "vmOperationTime",
PerfData::U_Ticks, CHECK);
}
} VMThread::VMThread() : NamedThread() {
set_name("VM Thread");
}

通过new VMThread()创建线程对象,在VMThread的构造方法里将线程命名成VM Thread,这就是我们jstack看到的VM Thread线程,同时还为这个线程创建了一个叫VMOperationQueue的队列。

至于VM Thread线程的作用,我们留到最后再说。

02 Signal Dispatcher线程

继续沿着 Threads::create_vm方法往下看,我们会看到如下代码:

// Signal Dispatcher needs to be started before VMInit event is posted
os::signal_init();

这一句代码实现了Signal Dispatcher线程的创建,进入到signal_init()方法看看:

void os::signal_init() {
if (!ReduceSignalUsage) {
// Setup JavaThread for processing signals
EXCEPTION_MARK;
Klass* k = SystemDictionary::resolve_or_fail(vmSymbols::java_lang_Thread(), true, CHECK);
instanceKlassHandle klass (THREAD, k);
instanceHandle thread_oop = klass->allocate_instance_handle(CHECK); const char thread_name[] = "Signal Dispatcher";
Handle string = java_lang_String::create_from_str(thread_name, CHECK); // Initialize thread_oop to put it into the system threadGroup
Handle thread_group (THREAD, Universe::system_thread_group());
JavaValue result(T_VOID);
JavaCalls::call_special(&result, thread_oop,
klass,
vmSymbols::object_initializer_name(),
vmSymbols::threadgroup_string_void_signature(),
thread_group,
string,
CHECK); KlassHandle group(THREAD, SystemDictionary::ThreadGroup_klass());
JavaCalls::call_special(&result,
thread_group,
group,
vmSymbols::add_method_name(),
vmSymbols::thread_void_signature(),
thread_oop, // ARG 1
CHECK); os::signal_init_pd(); { MutexLocker mu(Threads_lock);
JavaThread* signal_thread = new JavaThread(&signal_thread_entry); // At this point it may be possible that no osthread was created for the
// JavaThread due to lack of memory. We would have to throw an exception
// in that case. However, since this must work and we do not allow
// exceptions anyway, check and abort if this fails.
if (signal_thread == NULL || signal_thread->osthread() == NULL) {
vm_exit_during_initialization("java.lang.OutOfMemoryError",
"unable to create new native thread");
} java_lang_Thread::set_thread(thread_oop(), signal_thread);
java_lang_Thread::set_priority(thread_oop(), NearMaxPriority);
java_lang_Thread::set_daemon(thread_oop()); signal_thread->set_threadObj(thread_oop());
Threads::add(signal_thread);
Thread::start(signal_thread);
}
// Handle ^BREAK
os::signal(SIGBREAK, os::user_handler());
}
}

在这个方法里,我们可以看到要创建的线程名字:Signal Dispatcher,以及线程启动后调用的方法signal_thread_entry。(方法较长,看重点就好,没必要每句话都扣清楚)。

有了对上边代码的分析,我们只需要看看signal_thread_entry方法,就知道Signal Dispatcher线程的作用了。

static void signal_thread_entry(JavaThread* thread, TRAPS) {
os::set_priority(thread, NearMaxPriority);
while (true) {
int sig;
{
// FIXME : Currently we have not decieded what should be the status
// for this java thread blocked here. Once we decide about
// that we should fix this.
sig = os::signal_wait();//等待获取信号
}
if (sig == os::sigexitnum_pd()) {
// Terminate the signal thread
return;
} switch (sig) {
case SIGBREAK: {
// Check if the signal is a trigger to start the Attach Listener - in that
// case don't print stack traces.
if (!DisableAttachMechanism && AttachListener::is_init_trigger()) {
continue;
}
// Print stack traces
// Any SIGBREAK operations added here should make sure to flush
// the output stream (e.g. tty->flush()) after output. See 4803766.
// Each module also prints an extra carriage return after its output.
VM_PrintThreads op;
VMThread::execute(&op);
VM_PrintJNI jni_op;
VMThread::execute(&jni_op);
VM_FindDeadlocks op1(tty);
VMThread::execute(&op1);
Universe::print_heap_at_SIGBREAK();
if (PrintClassHistogram) {
VM_GC_HeapInspection op1(gclog_or_tty, true /* force full GC before heap inspection */);
VMThread::execute(&op1);
}
if (JvmtiExport::should_post_data_dump()) {
JvmtiExport::post_data_dump();
}
break;

这个方法里调用os::signal_wait()获取传给该jvm进程的信号,然后对信号进行处理。

说下case SIGBREAK里的处理逻辑,当接收到SIGBREAK信号时,会先判断是否禁止Attach机制,如果没有禁止,会调用AttachListener::is_init_trigger()方法触发Attach Listener线程的初始化.如果attach机制被禁用,则会创建VM_PrintThreads、VM_PrintJNI、VM_FindDeadlocks等代表某一个操作的对象,通过VMThread::execute()方法扔到VM Thread线程的VMOperationQueue队列。

03 Attach Listener线程

继续沿着 Threads::create_vm方法往下看,在紧挨着启动Signal Dispatcher线程的下边,就是启动Attach Listener线程的语句:

// Start Attach Listener if +StartAttachListener or it can't be started lazily
if (!DisableAttachMechanism) {
AttachListener::vm_start();
if (StartAttachListener || AttachListener::init_at_startup()) {
AttachListener::init();
}
}

重点就在AttachListener::init()方法里:

// Starts the Attach Listener thread
void AttachListener::init() {
EXCEPTION_MARK;
Klass* k = SystemDictionary::resolve_or_fail(vmSymbols::java_lang_Thread(), true, CHECK);
instanceKlassHandle klass (THREAD, k);
instanceHandle thread_oop = klass->allocate_instance_handle(CHECK); const char thread_name[] = "Attach Listener";
Handle string = java_lang_String::create_from_str(thread_name, CHECK); // Initialize thread_oop to put it into the system threadGroup
Handle thread_group (THREAD, Universe::system_thread_group());
JavaValue result(T_VOID);
JavaCalls::call_special(&result, thread_oop,
klass,
vmSymbols::object_initializer_name(),
vmSymbols::threadgroup_string_void_signature(),
thread_group,
string,
THREAD); if (HAS_PENDING_EXCEPTION) {
tty->print_cr("Exception in VM (AttachListener::init) : ");
java_lang_Throwable::print(PENDING_EXCEPTION, tty);
tty->cr(); CLEAR_PENDING_EXCEPTION; return;
} KlassHandle group(THREAD, SystemDictionary::ThreadGroup_klass());
JavaCalls::call_special(&result,
thread_group,
group,
vmSymbols::add_method_name(),
vmSymbols::thread_void_signature(),
thread_oop, // ARG 1
THREAD); if (HAS_PENDING_EXCEPTION) {
tty->print_cr("Exception in VM (AttachListener::init) : ");
java_lang_Throwable::print(PENDING_EXCEPTION, tty);
tty->cr(); CLEAR_PENDING_EXCEPTION; return;
} { MutexLocker mu(Threads_lock);
JavaThread* listener_thread = new JavaThread(&attach_listener_thread_entry); // Check that thread and osthread were created
if (listener_thread == NULL || listener_thread->osthread() == NULL) {
vm_exit_during_initialization("java.lang.OutOfMemoryError",
"unable to create new native thread");
} java_lang_Thread::set_thread(thread_oop(), listener_thread);
java_lang_Thread::set_daemon(thread_oop()); listener_thread->set_threadObj(thread_oop());
Threads::add(listener_thread);
Thread::start(listener_thread);
}
}
我们可以通过代码看出其创建了一个叫Attach Listener的线程,线程执行的逻辑封装在了attach_listener_thread_entry方法里。

Attach Listener线程的作用,我们看看attach_listener_thread_entry方法便知:

static void attach_listener_thread_entry(JavaThread* thread, TRAPS) {
os::set_priority(thread, NearMaxPriority); thread->record_stack_base_and_size(); if (AttachListener::pd_init() != ) {
return;
}
AttachListener::set_initialized(); for (;;) {
AttachOperation* op = AttachListener::dequeue();//从队列里获取操作对象
if (op == NULL) {
return; // dequeue failed or shutdown
} ResourceMark rm;
bufferedStream st;
jint res = JNI_OK; // handle special detachall operation
if (strcmp(op->name(), AttachOperation::detachall_operation_name()) == ) {
AttachListener::detachall();
} else {
// find the function to dispatch too
AttachOperationFunctionInfo* info = NULL;
for (int i=; funcs[i].name != NULL; i++) {
const char* name = funcs[i].name;
assert(strlen(name) <= AttachOperation::name_length_max, "operation <= name_length_max");
if (strcmp(op->name(), name) == ) {
info = &(funcs[i]);
break;
}
} // check for platform dependent attach operation
if (info == NULL) {
info = AttachListener::pd_find_operation(op->name());
} if (info != NULL) {
// dispatch to the function that implements this operation
res = (info->func)(op, &st);//执行操作对象
} else {
st.print("Operation %s not recognized!", op->name());
res = JNI_ERR;
}
} // operation complete - send result and output to client
op->complete(res, &st);
}
}
方法很长,我把重点挑出来分析。

首先我们看看调用AttachListener::pd_init()完了什么:

int AttachListener::pd_init() {
JavaThread* thread = JavaThread::current();
ThreadBlockInVM tbivm(thread); thread->set_suspend_equivalent();
// cleared by handle_special_suspend_equivalent_condition() or
// java_suspend_self() via check_and_wait_while_suspended() int ret_code = LinuxAttachListener::init(); // were we externally suspended while we were waiting?
thread->check_and_wait_while_suspended(); return ret_code;
} int LinuxAttachListener::init() {
char path[UNIX_PATH_MAX]; // socket file
char initial_path[UNIX_PATH_MAX]; // socket file during setup
int listener; // listener socket (file descriptor) // register function to cleanup
::atexit(listener_cleanup); int n = snprintf(path, UNIX_PATH_MAX, "%s/.java_pid%d",
os::get_temp_directory(), os::current_process_id());
if (n < (int)UNIX_PATH_MAX) {
n = snprintf(initial_path, UNIX_PATH_MAX, "%s.tmp", path);
}
if (n >= (int)UNIX_PATH_MAX) {
return -;
} // create the listener socket
listener = ::socket(PF_UNIX, SOCK_STREAM, );//创建套接字
if (listener == -) {
return -;
} // bind socket
struct sockaddr_un addr;
addr.sun_family = AF_UNIX;
strcpy(addr.sun_path, initial_path);
::unlink(initial_path);
int res = ::bind(listener, (struct sockaddr*)&addr, sizeof(addr));//绑定地址
if (res == -) {
::close(listener);
return -;
} // put in listen mode, set permissions, and rename into place
res = ::listen(listener, );//发起监听
if (res == ) {
RESTARTABLE(::chmod(initial_path, S_IREAD|S_IWRITE), res);
if (res == ) {
res = ::rename(initial_path, path);
}
}
if (res == -) {
::close(listener);
::unlink(initial_path);
return -;
}
set_path(path);
set_listener(listener); return ;
}

不难发现,AttachListener::pd_init()方法又调用了LinuxAttachListener::init()方法,完成了对套接字的创建和监听。这与jstack本地代码建立socket连接发送命令,不谋而合。

再就是有一个for死循环,不停地调用AttachOperation* op = AttachListener::dequeue();获取操作对象。如果进入到AttachListener::dequeue()方法看一看,其实就是在读上边监听的套接字,我这里就不贴源码了。

在这个死循环里,我们重点看看如下代码:

 // find the function to dispatch too
AttachOperationFunctionInfo* info = NULL;
for (int i=; funcs[i].name != NULL; i++) {
const char* name = funcs[i].name;
assert(strlen(name) <= AttachOperation::name_length_max, "operation <= name_length_max");
if (strcmp(op->name(), name) == ) {
info = &(funcs[i]);
break;
}
} // check for platform dependent attach operation
if (info == NULL) {
info = AttachListener::pd_find_operation(op->name());
} if (info != NULL) {
// dispatch to the function that implements this operation
res = (info->func)(op, &st);//调动方法
} else {
st.print("Operation %s not recognized!", op->name());
res = JNI_ERR;
}
} // operation complete - send result and output to client
op->complete(res, &st);

这个for循环会遍历funcs数组,然后根据从队列里拿到的AttachOperation对象的name来找到一个匹配的AttachOperationFunctionInfo对象,然后调用其func方法。

看到这里你或许很多疑惑,当然看看funcs数组里的东西,就开朗了:

static AttachOperationFunctionInfo funcs[] = {
{ "agentProperties", get_agent_properties },
{ "datadump", data_dump },
{ "dumpheap", dump_heap },
{ "load", JvmtiExport::load_agent_library },
{ "properties", get_system_properties },
{ "threaddump", thread_dump },
{ "inspectheap", heap_inspection },
{ "setflag", set_flag },
{ "printflag", print_flag },
{ "jcmd", jcmd },
{ NULL, NULL }
};

有没有看到上文中我们提到的threaddump命令。jstack通过与jvm进程建立socket连接,然后向jvm进程发送threaddump指令。上文说道调用AttachOperationFunctionInfo对象的func方法处理指令,其实就是调用了thread_dump方法,针对threaddump命令来说。

坚持,马上就要说完了。来看看thread_dump方法干了些啥吧:

// Implementation of "threaddump" command - essentially a remote ctrl-break
// See also: ThreadDumpDCmd class
//
static jint thread_dump(AttachOperation* op, outputStream* out) {
bool print_concurrent_locks = false;
if (op->arg() != NULL && strcmp(op->arg(), "-l") == ) {
print_concurrent_locks = true;
} // thread stacks
VM_PrintThreads op1(out, print_concurrent_locks);
VMThread::execute(&op1); // JNI global handles
VM_PrintJNI op2(out);
VMThread::execute(&op2); // Deadlock detection
VM_FindDeadlocks op3(out);
VMThread::execute(&op3); return JNI_OK;
}

很简单,创建了VM_PrintThreads、VM_PrintJNI、VM_FindDeadlocks三个对象,扔给了VM Thread线程的队列。

说到这里,VM Thread线程的作用,应该真相大白了,就是读取队列,然后执行相应的操作。有兴趣你可以继续追进去看看源代码,我这里就不追下去了。

总结

看了这么多代码,确实很头疼,总结下吧。

jstack是通过与jvm进程建立socket连接,然后发送指令来实现相关操作。

jvm的Attach Listener线程监听套接字,读取jstack发来的指令,然后将相关的操作扔给VM Thread线程来执行,最后返回给jstack。

在jvm启动的时候,如果没有指定StartAttachListener,Attach Listener线程是不会启动的,在Signal Dispatcher线程收到SIGBREAK信号时,会调用 AttachListener::is_init_trigger()通过调用用AttachListener::init()启动了Attach Listener 线程。


加入知识星球,可以有更多的交流,更多的学习和更快的提高。

聊聊jstack的工作原理的更多相关文章

  1. 聊聊Vim的工作原理

    聊聊Vim的工作原理 日常里一直在用Vim这个编辑器,前阵子学习关于Linux中的fd(文件描述符)时,发现vim的进程描述符会比上一个自动加一,后续了解到vim的工作原理后,解开了这个疑问,所以记录 ...

  2. 聊聊高并发(三十四)Java内存模型那些事(二)理解CPU快速缓存的工作原理

    在上一篇聊聊高并发(三十三)从一致性(Consistency)的角度理解Java内存模型 我们说了Java内存模型是一个语言级别的内存模型抽象.它屏蔽了底层硬件实现内存一致性需求的差异,提供了对上层的 ...

  3. java gc的工作原理、如何优化GC的性能、如何和GC进行有效的交互

    java gc的工作原理.如何优化GC的性能.如何和GC进行有效的交互 一个优秀的Java 程序员必须了解GC 的工作原理.如何优化GC的性能.如何和GC进行有效的交互,因为有一些应用程序对性能要求较 ...

  4. 图解WebGL&Three.js工作原理

    “哥,你又来啦?”“是啊,我随便逛逛.”“别介啊……给我20分钟,成不?”“5分钟吧,我很忙的.”“不行,20分钟,不然我真很难跟你讲清楚.”“好吧……”“行,那进来吧,咱好好聊聊” 一.我们讲什么? ...

  5. Tomcat性能优化及JVM内存工作原理

    Java性能优化原则:代码运算性能.内存回收.应用配置(影响Java程序主要原因是垃圾回收,下面会重点介绍这方面) 代码层优化:避免过多循环嵌套.调用和复杂逻辑. Tomcat调优主要内容如下: 1. ...

  6. 说一下Dubbo 的工作原理?注册中心挂了可以继续通信吗?

    面试题 说一下的 dubbo 的工作原理?注册中心挂了可以继续通信吗?说说一次 rpc 请求的流程? 面试官心理分析 MQ.ES.Redis.Dubbo,上来先问你一些思考性的问题.原理,比如 kaf ...

  7. Tomcat性能调优及JVM内存工作原理

    Java性能优化方向:代码运算性能.内存回收.应用配置. 注:影响Java程序主要原因是垃圾回收,下面会重点介绍这方面 代码层优化:避免过多循环嵌套.调用和复杂逻辑.Tomcat调优主要内容如下:1. ...

  8. [中英对照]How PCI Works | PCI工作原理

    How PCI Works | PCI工作原理 Your computer's components work together through a bus. Learn about the PCI ...

  9. android多线程-AsyncTask之工作原理深入解析(上)

    关联文章: Android 多线程之HandlerThread 完全详解 Android 多线程之IntentService 完全详解 android多线程-AsyncTask之工作原理深入解析(上) ...

随机推荐

  1. scrapy spider官方文档

    Spiders Spider类定义了如何爬取某个(或某些)网站.包括了爬取的动作(例如:是否跟进链接)以及如何从网页的内容中提取结构化数据(爬取item). 换句话说,Spider就是您定义爬取的动作 ...

  2. js实现两种实用的排序算法——冒泡、快速排序

      分类:js (4443) (0) 零:数据准备,给定数组arr=[2,5,4,1,7,3,8,6,9,0]; 一:冒牌排序 1思想:冒泡排序思想:每一次对比相邻两个数据的大小,小的排在前面,如果前 ...

  3. 一个关于C8051F350模拟电源的小问题

    前言 多做重要而不紧急的工作,慢慢的就会发现重要而紧急的工作没那么多了 工作方法 今天有好几个同事出差去现场实验了,为了今天的顺利成行,昨天加了个班,但是从项目管理的角度或者说做事的方法上来讲,这次加 ...

  4. sumo快速运行简单仿真实例详细教程

    本文旨在让大家快速的了解sumo,并给出运行一个简单的sumo的例子的教程,进而了解基本sumo工程的架构,使大家对该软件产生兴趣并持续学习下去,刚开始学习仿真的确枯燥,项目"跑起来&quo ...

  5. jQuery系列 第七章 jQuery框架DOM操作

    第七章 jQuery框架的选择器 jQuery框架继承和优化了JavaScript访问DOM对象的特性,我们使用jQuery框架提供的api可以更加方便的操作DOM对象. 7.1 创建DOM节点 使用 ...

  6. [LeetCode] Lonely Pixel II 孤独的像素之二

    Given a picture consisting of black and white pixels, and a positive integer N, find the number of b ...

  7. pat 1001 A+B Format

    题目链接:传送门 题目简述: 1. 给定两个整数值a,b: 2.范围-1000000 <= a, b <= 1000000: 3.按指定格式输出结果 例:-100000 9 输出: -99 ...

  8. [HNOI 2011]数矩形

    Description 题库链接 给出平面上 \(n\) 个点,选出四个点作为矩形顶点.求出矩形最大面积. \(1\leq n\leq 1500\) Solution 转载自 Z-Y-Y-S dark ...

  9. noip2017"退役"记

    day0 口胡了一下去年的六道题,感觉很稳,看了6集动漫,0点钟就去睡了. day1 早上被一阵革命练习曲吵醒,而我还是窝在被子里不想起床(-﹃-)~zZ.于是室友开始放起了lost river... ...

  10. 【BZOJ2809】【APIO2012】派遣

    Background 在一个忍者的帮派里,一些忍者们被选中派遣给顾客,然后依据自己的工作获取报偿. Description 在这个帮派里,有一名忍者被称之为Master.除了Master以外,每名忍者 ...